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

After combing through the SVG specification, and guides such as this and this, I am still struggling to understand exactly how chaining transforms work.

Selected Relevant Quotes

When you apply the transform attribute to an SVG element, that element gets a "copy" of the current user coordinate system in use.

And:

When transformations are chained, the most important thing to be aware of is that, just like with HTML element transformations, each transformation is applied to the coordinate system after that system is transformed by the previous transformations.

And:

For example, if you’re going to apply a rotation to an element, followed by a translation, the translation happens according to the new coordinate system, not the inital non-rotated one.

And:

The sequence of transformations matter. The sequence the transformation functions are specified inside the transform attribute is the sequence they are applied to the shape.

Code

The first rectangle's current coordinate system is scaled, then rotated (note the order). The second rectangle's current coordinate system is rotated, then scaled.

svg {
  border: 1px solid green;
}
<svg xmlns="http://www.w3.org/2000/svg">
  <style>
    rect#s1 {
      fill: red;
      transform: scale(2, 1) rotate(10deg);
    }
  </style>
  <rect id="s1" x="" y="" width="100" height="100" />
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
  <style>
    rect#s2 {
      fill: blue;
      transform: rotate(10deg) scale(2, 1);
    }
  </style>
  <rect id="s2" x="" y="" width="100" height="100" />
</svg>

Question

We know that when we chain transforms, a copy is made of the current coordinate system in use for that element, then the transforms are applied in the order they are specified.

When we have a user coordinate system that is already scaled, and we apply a rotate to it, the rectangle is (as seen) effectively skewed (notice the changed angles). This does not happen if we do the two transforms the other way around (rotate, then scale).

Expert help on exactly how the scaled current coordinate system is rotated, would be deeply appreciated. I am trying to understand, from a technical (inner workings) angle, exactly why the skewing happens in the first rectangle.

Thank you.


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) 

The way Temani Afif explained it follows the coordinate systems that every transformation spans. You start with the viewport, and each consecutive coordinate system is derived and sits somewhere different on the canvas. These coordinate systems might turn out not be cartesian (a "stretched universe"). They are constructed in the DOM tree from the outside in, and when chained in an attribute, from left to right.

But you can imagine the same transformation also in the opposite direction, from the inside out: first you draw a rectangle in its cartesian userspace coordinate system, and than you transform it by a chain of scales, rotations and so on, until when drawing it in the viewport coordinate system, it is distorted to something else.

But if you look at it this the second way, the chained transformations in an attribute need to be processed right to left: transform: scale(2, 1) rotate(10deg) means take a rectangle, first rotate it by 10deg, and then scale the rotated rectangle in the horizontal direction.

In short, these two are equivalent:

  • If you draw a grafic in a transformed coordinate system, construct the coordinate system by applying transforms to these coordinate systems left-to-right.
  • If you draw a transformed grafic in the original coordinate system, construct the grafic by applying transforms to the grafic right-to-left.