5
votes

I thought I had pretty decent understanding of SVG, including the viewport, viewBox and the user coordinate system.

In the first example below, we use a viewBox with the same aspect ratio as the viewport. As expected, the user coordinate system rotation does not distort any angles.

In example two, we set the viewbox to a different aspect ratio, compared to the viewport. In other words, when mapping the viewBox to the viewport, the shapes' aspect ratios are not maintained. The bottom-right angle is not distorted from this scaling, which makes sense since the coordinate system origin is at (0,0).

When we rotate the user coordinate system in example two, however, the bottom right angle is distorted. This does not happen in example one.

Edit 1: Just to be clear, the issue is with regards to the bottom right angle in the last example. Before rotating, but after stretching with viewBox, the angle is 90%. After rotating however, it is no longer 90%.

Why does a non-uniformly scaled triangle loose its angles when rotating?

Example One (uniform scale)

body {
  height: 500px;
}

svg {
  width: 400px;
  height: 400px;
  border: 1px solid red;
}
<svg id="s1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 200 200" preserveAspectRatio="none">
    <style>
      polygon {
        transform: translate(100px, 0px);
        animation: 2s ease-in 1s 1 normal forwards rotate-down;
        fill: green;
      }
      
      @keyframes rotate-down {
        0% {
          transform: translate(100px, 0px) rotate(0deg);
        }
        100% {
          transform: translate(100px, 0px) rotate(45deg);
        }
      }
    </style>
    <polygon points="100,100 100,0 0,100" />
  </svg>

Example Two (non-uniform scale)

body {
  height: 500px;
}

svg {
  width: 600px;
  height: 400px;
  border: 1px solid red;
}
<svg id="s1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 200 400" preserveAspectRatio="none">
    <style>
      polygon {
        transform: translate(100px, 0px);
        animation: 2s ease-in 1s 1 normal forwards rotate-down;
        fill: green;
      }
      
      @keyframes rotate-down {
        0% {
          transform: translate(100px, 0px) rotate(0deg);
        }
        100% {
          transform: translate(100px, 0px) rotate(45deg);
        }
      }
    </style>
    <polygon points="100,100 100,0 0,100" />
  </svg>

EDIT 2 (images to clarify):

Below we see the triangle after viewBox has been added (thus scaled and translated), but before rotating. The bottom right angle is 90 degrees.

enter image description here

Below we see the triangle after viewBox has been added (thus scaled and translated), and after rotating. The bottom right angle is no longer 90 degrees.

enter image description here


EDIT 3:

I eventually got to the bottom of this.

Below is an answer explaining the details and linking to relevant resources.

3
Where do you see distorted angle? The triangle is just stretched. How would you like to stretch triangle and maintain 90 deg angle at the same time? Stretching is changing angle (at least for rectangles). - Mailosz
@Magnus but it is the stretching that does that. - Robert Longson
@enxaneta I fully get what preserveAspectRatio does, and indeed how viewBox in combination with that attribute scales the graphics within the current user coordinate system (UCS). That is not where my confusion lies. I thought the viewBox was first applied, THEN other transforms are applied to a copy of the modified UCS created by viewBox. I have been through that part of the spec a few times, thus that's where my understanding came from. - Magnus
"The transform attribute establishes a new user space". It establishes a new user coordinate system within the element. But it doesn't over-write the current one. It is still affected by other transforms that have been applied outside of it. For example, everything inside the SVG is affected by the viewBox transform no matter what other transforms happen inside the SVG. - Paul LeBeau
don't make an edit to add a solution/answer .. answer you own question instead - Temani Afif

3 Answers

2
votes

Hopefully this example will show you what's going on.

Hover over the SVG to see why it is the stretching that is changing the angle.

body {
  height: 500px;
}

svg {
  width: 200px;
  height: 400px;
  border: 1px solid red;
  transition: 1s width;
}

svg:hover {
  width: 600px;
}
<svg id="s1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 200 400" preserveAspectRatio="none">
    <style>
      polygon {
        transform: translate(100px, 0px) rotate(45deg);
        fill: green;
      }
    </style>
    <polygon points="100,100 100,0 0,100" />
  </svg>
2
votes

I finally got to the bottom of this.

The following question, which I posted after concluding what the actual problem was, explains why coordinate transformations behave as they do:

In an answer to that question, @TemaniAfif shows how the final transformation matrix is calculated and applied to the graphic element's coordinates, in order to map it from the viewport coordinate system to the final user coordinate system.

Long story short, when applying transformations, what we actually do is copying the current user coordinate system, then translating it in relation to the current user coordinate system we copied from. In SVG, the initial user coordinate system (before viewBox or any transforms) is identical to the initial viewport coordinate system.

The chained / nested transforms are applied to the coordinate system left-to-right / outside-in, to reach a final coordinate system within which the graphical elements can be mapped. Note that nesting transforms have the same effect as chaining transforms on one element.

How does this actually work? Well, every transformation has an pre-defined affine transformation matrix, not related to CSS/SVG. There are several Wikipedia articles showing the matrices, like:

To map the coordinates of an element to a final user coordinate system, we multiple the matrices with each other, left-to-right (the order it was written in the source code), to reach the final transformation matrix.

Note that, since we multiply the transform matrices in the order they are written in our source code, and since AxB is different from BxA when multiplying matrices, the order in which the transformations are written in our source code matters.

Finally, we then multiply the x and y coordinates for our element with this final transformation matrix, to see how each coordinate is mapped from the viewport coordinate system to the final user coordinate system.

For those so inclined, it might be easier to not think about the above and instead just mentally imagine that the chained / nested transforms are applied to the element itself (not to user coordinate systems) right-to-left / inside-out (i.e. opposite order of how it was applied to the coordinate systems).

Whether you imagine mentally that you transform the coordinate systems left-to-right and then map in the graphic element, or you transform the element itself by applying the transforms right-to-left, the end result will be the same.

Relevant Specifications

Note

For this question it does not really matter whether the transforms are applied to SVG elements or to HTML elements. The same transformation mechanics apply.

-1
votes

It seems you think that viewBox is some kind of transform method applied, like others, when computing SVG image, which is not true. What you experience here is transformation applied on the whole SVG element. To apply this transformation a browser needs to have SVG object computed, so all in-SVG transformations are already applied.

This works exactly as scaling raster images:

polygon {
  fill: transparent;
  stroke-width: 4px;
  stroke: black;
}
Base raster image:<br>
<img src="https://i.stack.imgur.com/ZA16O.png">

<br>Stretched raster image:<br>
<img style="height: 150px; width: 300px" src="https://i.stack.imgur.com/ZA16O.png">

<br>Base SVG:<br>
<svg viewBox="0,0,160,160" style="height: 120px" preserveAspectRatio="none">
<polygon points="10,10 150,10 80,150"/>
</svg>

<br>Stretched SVG:<br>
<svg viewBox="0,0,160,160" preserveAspectRatio="none" style="height: 120px; width: 300px;">
<polygon points="10,10 150,10 80,150"/>
</svg>

Only SVG's are drawed after they have been transformed, hence they do not lose quality.


SVG spec actually says (here) that all the transforms applied to SVG element work that way.