Why does order of transforms matter? rotate/scale doesn’t give the same result as scale/rotate

To illustrate how it works let’s consider an animation to show how the scaling effect change the rotation.

.red {
  width:80px;
  height:20px;
  background:red;
  margin:80px;
  transform-origin:left center;
  animation: rotate 2s linear infinite;
}
@keyframes rotate {
  from{transform:rotate(0)}
  to{transform:rotate(360deg)}

}
<div class="container">
<div class="red">
</div>
</div>

As you can see above, the rotation is creating a perfect circle shape.

Now let’s scale the container and see the difference:

.red {
  width:80px;
  height:20px;
  background:red;
  margin:80px;
  transform-origin:left center;
  animation: rotate 5s linear infinite;
}
@keyframes rotate {
  from{transform:rotate(0)}
  to{transform:rotate(360deg)}

}
.container {
  display:inline-block;
  transform:scale(3,1);
  transform-origin:left center;
}
<div class="container">
<div class="red">
</div>
</div>

Notice how we no more have a circle but it’s an ellipse now. It’s like we took the circle and we stertch it which is creating the skew effect inside our rectangle.


If we do the opposite effect and we start by having a scale effect and then we apply a rotation we won’t have any skewing.

.red {
  width:80px;
  height:20px;
  background:red;
  margin:80px;
  animation: rotate 2s linear infinite;
}
@keyframes rotate {
  from{transform:scale(1,1)}
  to{transform:scale(3,1)}

}
.container {
  display:inline-block;
  transform:rotate(30deg);
  transform-origin:left center;
}
<div class="container">
<div class="red">
</div>
</div>

To explain it differently: Applying a rotation will keep the same ratio between both X and Y axis so you won’t see any bad effect when doing scale later but scaling only one axis will break the ratio thus our shape we look bad when we try to apply a rotation.


You can check this link if you want more details about how transform are chained and how the matrix is caclulated: https://www.w3.org/TR/css-transforms-1/#transform-rendering. It’s about HTML element but as said in the SVG specification it’s the same.

Here is the relevant parts:

Transformations are cumulative. That is, elements establish their local coordinate system within the coordinate system of their parent.

From the perspective of the user, an element effectively accumulates all the transform properties of its ancestors as well as any local transform applied to it


Let’s do some math in order to see the difference between both transformations. Let’s consider matrix multiplication and since we are dealing with a 2D linear transformation we will do this on ℝ² for simplicity1.

For scale(2, 1) rotate(10deg) we will have

 |2 0|   |cos(10deg) -sin(10deg)|   |2*cos(10deg) -2*sin(10deg) |
 |0 1| x |sin(10deg) cos(10deg) | = |1*sin(10deg) 1*cos(10deg)  |

Now if we apply this matrix to an (Xi,Yi) we will obtain (Xf,Yf) like below:

 Xf = 2* (Xi*cos(10deg) - Yi*sin(10deg))
 Yf =     Xi*sin(10deg) + Yi*cos(10deg)

Note how the Xf is having an extra multiplier which is the culprit of creating the skew effect. It’s like we changed the behavior or Xf and kept the Yf

Now let’s consider rotate(10deg) scale(2, 1):

 |cos(10deg) -sin(10deg)|   |2 0|   |2*cos(10deg) -1*sin(10deg) |
 |sin(10deg) cos(10deg) | x |0 1| = |2*sin(10deg) 1*cos(10deg)  |

And then we will have

 Xf =  2*Xi*cos(10deg) - Yi*sin(10deg)
 Yf =  2*Xi*sin(10deg) + Yi*cos(10deg)

We can consider the 2*Xi as an Xt and we can say that we rotated the (Xt,Yi) element and this element was initially scaled considering the X-axis.


1CSS uses also affine transformation (like translate) so using ℝ² (Cartesian coordinates) isn’t enough to perform our calculation so we need to consider ℝℙ² (Homogeneous coordinates). Our previous calculation will be:

 |2 0 0|   |cos(10deg) -sin(10deg) 0|   |2*cos(10deg) -2*sin(10deg) 0|
 |0 1 0| x |sin(10deg) cos(10deg)  0| = |1*sin(10deg) 1*cos(10deg)  0|
 |0 0 1|   |0          0           1|   |0            0             1|

Nothing will change in this case because the affine part is null but if we have a translation combined with another transform (ex: scale(2, 1) translate(10px,20px)) we will have the following:

 |2 0 0|   |1 0 10px|   |2 0 20px|
 |0 1 0| x |0 1 20px| = |0 1 20px|
 |0 0 1|   |0 0 1   |   |0 0  1  |

And

Xf =  2*Xi + 20px;
Yf =  Yi + 20px;
1  =  1 (to complete the multiplication) 

Leave a Comment