5

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.

Magnus
  • 6,791
  • 8
  • 53
  • 84
  • 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 Dec 07 '18 at 10:06
  • @Mailosz Bottom right angle in last example. Before rotating, but after stretching with viewBox, the angle is 90%. After rotating, it is no longer 90%. Added a clarification to the post. – Magnus Dec 07 '18 at 10:08
  • The value of the viewBox attribute is a list of four numbers min-x, min-y, width and height, as per my analysis you have a square in which your svg animation will take place but in second example you have a rectangle viewbox i.e. 0 0 200 400. So now to make your animation complete the triangle you made adjust istelf to make that animation happen as the angle of triangle are not defined, the animation is taking place on the basis of coordinates you have provided, and animation is being completed. So if you want that angle to not get distorted you will have to maintain ratio or coordinates. – Joykal Infotech Dec 07 '18 at 10:23
  • @Magnus It's still 90 deg, only stretched. It seems to me that you indeed have "pretty decent understanding of SVG", but no understanding of how geometry works. – Mailosz Dec 07 '18 at 10:26
  • @Mailosz Seems we might be talking past each other. The stretching as you say, is caused by viewBox (mapping it to the viewport effectively scales then translates the user coordinate system). After that stretching, the bottom right angle is still 90 degrees (see the newly added drawings to my OP). Then, we rotate. We do not do anything else. As seen from the second image added, the angle is now no longer 90 degrees. – Magnus Dec 07 '18 at 10:47
  • what if you adjust the height and width of svg ? – godfather Dec 07 '18 at 11:07
  • I fail to see what disturbs you. Use a square or even a circle, you might get a better glance at what happens. – Kaiido Dec 07 '18 at 11:13
  • @godfather I am just looking for an understanding of why rotating the triangle changes the angle. It is not the stretching that does that. The stretching (which technically is scaling), happens due to the viewBox. The rotation is applied to the user coordinate system **after** that scaling has taken place. – Magnus Dec 07 '18 at 11:22
  • You are using `preserveAspectRatio="none"` . This and the fact that you don't have the same aspect ratio is distorting the image. The image is stretched or squished to fit the height and width you give it. – enxaneta Dec 07 '18 at 11:26
  • 1
    @Magnus but it **is** the stretching that does that. – Robert Longson Dec 07 '18 at 11:29
  • Hi @RobertLongson Thanks for dropping in. I thought after the viewBox was applied, we ended up with a scaled (stretched) user coordinate system (UCS). Then the rotate transform makes a copy of that UCS and simply rotates it. Since the angles does not look distorted without rotate (but still with viewBox), I was a bit surprised. I am misunderstanding something about the order / mechanics of how the transforms are applied it seems. Is my above understanding wrong? – Magnus Dec 07 '18 at 11:35
  • 1
    @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 Dec 07 '18 at 11:41
  • let me jump on this too :p .. honestly it seems fine to me and as @RobertLongson said, this is the stretch effect. You have a scale on one axis and initially the angle was on the opposite axis so there is no stretch until you rotate the shape – Temani Afif Dec 07 '18 at 11:46
  • basically it's exactly like you apply the transform initially : https://jsfiddle.net/8rzbjk3m/ .. .even when using SVG transform:https://jsfiddle.net/8rzbjk3m/2/. Since everything is inside the SVG, the browser will recalculate everything again – Temani Afif Dec 07 '18 at 11:56
  • Hi @TemaniAfif . Check out this pen: https://codepen.io/magnusriga/pen/EOqqxb . I have spent a substantial amount of time on the SVG spec and Sara's guide (https://www.sarasoueidan.com/blog/svg-coordinate-systems/) to fully understand how viewBox and transforms work, in relation to the two coordinate systems created by the svg, namely the viewport coordinate system and the user coordinate system. As Sara pointed out, and the spec confirms viewBox will first scale (and if x y is given, also translate it). Any subsequent transforms will make a copy of that UCS and apply its transform there. – Magnus Dec 07 '18 at 11:58
  • I don't see it like this, for me there is no *after* viewbox or before ... applying transform will make the browser to redo everything. Exactly the same thing happen with canvas, you can scale the canvas and apply some transformation to element inside, they will get affected by the scaling effect (here is a canvas example : https://stackoverflow.com/questions/53608436/drawing-line-on-canvas-unexpected-scale?noredirect=1&lq=1) – Temani Afif Dec 07 '18 at 12:03
  • well, I don't know the SVG spec, but I am explaining this based on how logically It should be. Probably the Spec is somehow confusing but I am pretty sure it should be like that .. Will try to dig into it and find where the confusion is – Temani Afif Dec 07 '18 at 12:04
  • @TemaniAfif In this guide: https://www.sarasoueidan.com/blog/svg-transformations/ you will read: "The transform attribute establishes a new user space (current coordinate system) on the element it is applied to.". Now, in the spec, you can see that the ONLY two ways to create a new user coordinate system is via viewBox and transform. The spec: https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute – Magnus Dec 07 '18 at 12:07
  • @RobertLongson In the spec, does the below paragraph related to just the top-level svg element, or individual containing shapes as well? "If both transform (or ‘patternTransform’) and ‘viewBox’ are applied to an element two new coordinate systems are established. transform establishes the first new coordinate system for the element. ‘viewBox’ establishes a second coordinate system for all descendants of the element. The first coordinate system is post-multiplied by the second coordinate system." Link: https://www.w3.org/TR/SVG/coords.html – Magnus Dec 07 '18 at 12:09
  • https://dxr.mozilla.org/mozilla-central/source/dom/svg/SVGViewportElement.cpp#260 GetTransformInternal is the transform/patternTransform. GetViewBoxTransform is what you'd expect. – Robert Longson Dec 07 '18 at 12:18
  • So you think that rotation should be happening after the stretch because you added it later? Every time you alter SVG (eg. with animation) the whole process is applied again. So: 1) rotate triangel, and then 2) stretch it to fill view – Mailosz Dec 07 '18 at 12:19
  • 1
    "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 Dec 07 '18 at 12:25
  • @Mailosz Yes, that is exactly what I think. Transformations are chainable and nestable. Every new transformation creates a copy of the previous current user coordinate system, and then applies to that copy. I have replicated viewBox behavior with transforms in this pen: https://codepen.io/magnusriga/pen/EOqqxb – Magnus Dec 07 '18 at 12:38
  • But viewBox is not a transformation in the way you think it is. It is not a part of the "chain". It is always at the end, just before browser is drawing SVG. – Mailosz Dec 07 '18 at 13:46
  • @Mailosz I actually do not think that is right. I spent a lot of time digging through the spec on this. Here is proof: https://codepen.io/magnusriga/pen/EOqqxb . Compare the first svg to the second to last svg, and you will see they are identical. One is using viewBox, the other is showing how viewBox works by chaining `scale(..)`. If you take the `scale(2, 1)` in the second to last svg and place it at the end of the chain (after `translate`), you will see that it no longer matches the first svg. It is, as the spec explains, a set of chained transformations. – Magnus Dec 07 '18 at 13:51
  • All the relevant information is here: https://www.w3.org/TR/SVG/coords.html – Magnus Dec 07 '18 at 13:54
  • @Magnus: But what you do not get is that specifying viewBox does NOT stretch the graphic, and as such is not a transformation. The stretch is applied _after_ all the graphic has been computed (but before it has been drawn) because the width of element is set to different value than vieBox. – Mailosz Dec 07 '18 at 15:18
  • @Mailosz That is actually wrong. The viewBox does stretch the graphic. See the spec: "The presence of the ‘viewBox’ attribute results in a transformation being applied to the viewport coordinate system as described in Computing the equivalent transform of an SVG viewport." Source: https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute . I fully understand the viewBox attribute, it is actually irrelevant here. The issue is around the order of transforms. You can see the same problem here: https://stackoverflow.com/questions/53671968/svg-rotate-after-scale-order-of-transforms – Magnus Dec 07 '18 at 15:21
  • @Magnus: you misunderstood my point. I tried to clarify it in an answer. – Mailosz Dec 07 '18 at 16:00
  • 1
    don't make an edit to add a solution/answer .. answer you own question instead – Temani Afif Dec 09 '18 at 21:18
  • @TemaniAfif Hmmm ok, will fix. – Magnus Dec 09 '18 at 21:22

3 Answers3

2

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>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
  • Thanks Paul. To narrow down my issue, I created the following pen where viewBox is replaced by `transform: scale(2,1)` (which has same effect): https://codepen.io/magnusriga/pen/EOqqxb . We then go on to add `translate(50px, 20px)` and finally `rotate(45deg)`. Could we go through this step by step? First, a copy of the initial user coordinate system (UCS) is created and scaled, doubled in width and no change to height. 2) The we shift that whole coordinate system to the right 50px and down 20px. Due to the previously applied scale, this is actually 100px and 20px in the viewport. (1/2) – Magnus Dec 07 '18 at 13:45
  • ... 3) Then we add rotation of 45 degrees. Somehow, that is applied differently to the x-axis vs the y-axis. Perhaps coordinates along the x-axis are rotated twice as much as the y-axis, due to the same principle as what happened to the previous translate. – Magnus Dec 07 '18 at 13:48
  • You have it back to front. You can think of transforms as being applied from last to first. So first the polygon is rotated 45deg, then it is moved 50 right and 20 down, and finally it is scaled horizontally by 2x. – Paul LeBeau Dec 07 '18 at 14:06
  • Thanks Paul. Do you know where in the spec that behavior is stated? This popular SVG guide seems to indicate the opposite: https://www.sarasoueidan.com/blog/svg-transformations/#nested-and-chained-transformations . Actually, this guide as well states the exact opposite: http://tutorials.jenkov.com/svg/svg-transformation.html#combining-transformations ("The sequence the transformation functions are specified inside the transform attribute is the sequence they are applied to the shape.") – Magnus Dec 07 '18 at 14:28
  • It depends whether you are thinking about it from the coordinate space's perspective, or the object's perspective. The transforms are being applied from left to right to the coordinate spaces. However if you are thinking about it from the point of view of the object, it's easier to wrap your head around it by going from inside out. To work out the final position of the square, apply it's transform first, then its parent's, then up through all of its ancestors. – Paul LeBeau Dec 07 '18 at 14:44
  • @Magnus I think at the end, your issue is to understand the orders of things. This may probably help you : https://stackoverflow.com/questions/51077632/simulating-transform-origin-using-translate/51080643#51080643 ... it's not about SVG but It should be the same. – Temani Afif Dec 07 '18 at 14:50
  • I posted another related question at the following link, which is a real minimal working example demonstrating the issue: https://stackoverflow.com/questions/53671968/svg-rotate-after-scale-order-of-transforms – Magnus Dec 07 '18 at 14:59
  • @TemaniAfif I always have the current user coordinate system in mind. That is different in SVG vs HTML. In HTML, every element has a coordinate system based in itself. In SVG, every element's current user coordinate system is a copy of what came before it (be it nested or chained), all the way back to the initial user coordinate system. Origin in SVG is at (0,0) another aspect that differs from HTML. I always think about where a transform leaves the user coordinate system. See: https://www.sarasoueidan.com/blog/svg-transformations/#coordinate-system-transformations – Magnus Dec 07 '18 at 15:07
  • @PaulLeBeau I am thinking solely about the coordinate systems, both the viewpoint coordinate system and the *user coordinate system* (aka. *current coordinate system*, aka. *user space* in use). I prefer to avoid abstractions, as in reality, what actually happens is that the entire user coordinate system is modified, one transformation after the other. I am trying to understand how rotate works on the user coordinate system. Does it rotate each pixel, thus x and y coordinates are rotated differently on a non-uniformly scaled object... Hmm – Magnus Dec 07 '18 at 15:13
  • @Magnus I think i got your confusion ... all SVG element have the same coordinate system but when you apply a transform to one element you will not apply to the Coordiante system shared by everyone, so you copy it and you used for the transformed element so it like the transformed element has it's own coordinate exactly like any HTML element – Temani Afif Dec 07 '18 at 15:15
  • @TemaniAfif Yep, I am very aware of that. The confusion is how exactly the rotation of a scaled coordinate system, leads to the element inside being skewed. Presumably it should be rotated together with its current coordinate system. – Magnus Dec 07 '18 at 15:18
  • @Magnus it doesn't and thereby hangs your problem. Draw a triangle that has no sides perpendicular to an axis, stretch one axis. The angles change. No rotation required. – Robert Longson Dec 07 '18 at 15:49
  • "I prefer to avoid abstractions". It's not an abstraction It all depends on your point of view. If you are on a spinning merry-go-round, then from your point of view, you are standing still (in your coordinate system) and the world is spinning. But to anyone on the outside, you are the one that's rotating. – Paul LeBeau Dec 07 '18 at 17:19
  • Thanks Paul. I made an edit to my question, laying forth a full solution to my confusion. – Magnus Dec 09 '18 at 21:02
  • As suggested by Temani, my edit should be its own answer instead. Sorry for the back and forth. – Magnus Dec 09 '18 at 21:22
2

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.

Magnus
  • 6,791
  • 8
  • 53
  • 84
-1

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.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
Mailosz
  • 131
  • 1
  • 9
  • I got to the bottom of my confusion. If interested, the answer is now in the OP. – Magnus Dec 09 '18 at 21:06
  • viewBox was not related to the question, but since you bring it up I have to note that both viewBox and transforms (single, chained or nested), will create a copy of the current user coordinates system, then modify it. The graphic element is not mapped into this coordinate system until the final transformation matrix has been computed (via dot product of the affine transformation matrices). – Magnus Dec 09 '18 at 21:08
  • The effect of viewBox is the same as a transform on the svg element itself. I will post the relevant parts of the spec in separate comments. – Magnus Dec 09 '18 at 21:09
  • *The presence of the ‘viewBox’ attribute results in a transformation being applied to the viewport coordinate system as described in "Computing the equivalent transform of an SVG viewport".* - https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute . The last mentioned section showing the transformation that viewBox implies, is here: https://www.w3.org/TR/SVG/coords.html#ComputingAViewportsTransform – Magnus Dec 09 '18 at 21:10
  • *The effect of the ‘viewBox’ attribute is that the user agent automatically supplies the appropriate transformation matrix to map the specified rectangle in user coordinate system to the bounds of a designated region (often, the SVG viewport).* - https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute – Magnus Dec 09 '18 at 21:11
  • *Note that in some cases the user agent will need to supply a translate transformation in addition to a scale transformation. For example, on an outermost svg element, a translate transformation will be needed if the ‘viewBox’ attributes specifies values other than zero for or .* - Same source link as above. – Magnus Dec 09 '18 at 21:11
  • As you note, `viewBox` is a transform applied to the whole svg element. That part is correct. However, nesting transforms is no different than chaining them for a particular element. All the transforms (including `viewBox`) are made to the coordinate systems (not the element itself), outside-in / left-to-right. Once the final transform matrix is ready, we can go ahead and map the element into the final user coordinate system. The details are here: https://www.w3.org/TR/css-transforms-1/#transform-rendering – Magnus Dec 09 '18 at 21:42
  • @Magnus: Whoa, that is a lot of comments :) "However, nesting transforms is no different than chaining them for a particular element" I agree, what I wanted to say is that if you change viewport it does not perform transformations on shapes it contains, but on the whole image generated by the tag. – Mailosz Dec 10 '18 at 10:13