Search code examples
csssvgcss-transforms

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.


Solution

  • 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)