2

So I know there are threads about it already here, like that one. I followed the idea that was proposed in the thread above, and it works. However, I don't understand WHY it works.

Here is an example:

Let's say that i have a square centered at (100, 100), and its width/height is 100. So its top-left corner will be at (50, 50).

Now let's say that i want to zoom X2 into the square's center, that is, to zoom into (100, 100). So i will write the following transformation sequence:

translate(100, 100);
scale(2, 2);
translate(-100, -100);

So because the canvas apply the transformation in reverse order, my transformed square's top-left corner will be now at (0, 0), and its height/width will be 200.

Ok, let's say that now i want to zoom X2 into the right-bottom corner of the already transformed square. So intuitively, i would like to perform the following transformation sequence:

translate(200, 200);
scale(2, 2);
translate(-200, -200);

But it wont work, because again, the canvas apply transfomations in reverse order. That is to say, that if i sum up my two transformation sequences, i'll get:

// First Sequence
translate(100, 100);
scale(2, 2);
translate(-100, -100);

// Second Sequence
translate(200, 200);
scale(2, 2);
translate(-200, -200);

This means that the second sequence will be applied to each point before the first sequence (because the canvas will apply the transformation from bottom to top), and this is wrong. So as the thread in the link above suggest the following:

Because sequence 2 will be applied first, i should transform the point (200, 200) to its original coordinates, by applying to it the inverse of the first sequence. that is to say, if T1 is the matrix that represents the first sequence, then it will look like that:

// First Sequence
translate(100, 100);
scale(2, 2);
translate(-100, -100);

// Second Sequence
var point = SVGPoint(200, 200);
var transformedPoint = point.matrixTransform(T1.inverse());
translate(-transformedPoint.x, -transformedPoint.y);
scale(2, 2);
translate(transformedPoint.x, transformedPoint.y);

But why it works? I really don't understand why it should work like that... can anyone elaborate about it?

Thanks!

Community
  • 1
  • 1
gipouf
  • 1,221
  • 3
  • 15
  • 43

2 Answers2

3

The HTML5 canvas transformations happen top-down, not bottom-up as you believe. The reason for the distinction is because the transformations applied to the canvas affect the coordinate system, not your logical coordinates.

Translating by translate(100, 100) will move your coordinate system right and down, which appears hauntingly similar to moving your logical coordinate up and left.

Let's take the first sequence (I have changed your use of transform to translate):

translate(100, 100);
scale(2, 2);
translate(-100, -100);

Naturally, when we think to scale an object from it's center, we translate the object to (0,0), scale the object, then move the object back. The above code, when read in reverse, would appear to do that. However, that's not the case.

When we read the above code from top-down, it says (assume we start with an identity transform):

  1. Move the context's (0,0) right 100 units and down 100 units. This takes it to the canvas's (100,100) location.
  2. Make the coordinate system 2x bigger.
  3. Move the context's (0,0) left 100 units and up 100 units, essentially returning it to it's original location (in context coordinate space, not canvas space).

The scaling happens relative to the context's (0,0) point, which is at (100,100) on the canvas.

If we were to now add your second sequence:

translate(200, 200);
scale(2, 2);
translate(-200, -200);

This will:

  1. Move the context's (0,0) to the coordinate system's (200,200) location.
  2. Make the coordinate system 2x bigger than it already was.
  3. Return the context's (0,0) back to where it was previously (in context coordinate space, not canvas space).

As you've found out, that does not give you what you are expecting because (200,200) is not the point about which you want to scale. Remember, all units are relative to the context coordinate system. So we need to convert the canvas location of (200,200) to the context coordinate location of (150,150) which is the original bottom-right corner of our rectangle.

So we change sequence #2 to be:

translate(150, 150);
scale(2, 2);
translate(-150, -150);

This gives us what we are expecting (to zoom in on the bottom-right corner of the rectangle). That's why we do the inverse-transform.

In the demo application, when the app zoom's in, it's taking the coordinate in canvas units where the user's mouse was, inverse-transforming that using the context transformation thus-far, to get the location in context coordinate space that was clicked on. The context origin is moved to that location, zoomed, then returned to it's previous location.

References:

Matt Houser
  • 33,983
  • 6
  • 70
  • 88
  • Hi! Thanks, That's a good explanation which really helped me for the case of looking at transformations as they are applied to the coordinate system. But there is still something missing for me before i accept this answer - I would want to kindly ask you to delve into it a bit more with me - In practice, the canvas implementation DO apply transformations on the logical points themselves with a transformation matrix - [PLEASE CONTINUE READING THE THE NEXT COMMENT] – gipouf May 27 '13 at 09:54
  • Those transformations are applied in reverse order, in order to give you the illusion that when looking on the transformations in their original order, they will work on the coordinate system. Am i right here? This is the main issue that bothers me. They are applied in reverse order because the canvas's current transformation matrix is post-multiplied with each new transformation that the user apply... so in practice, each logical point will first be transformed with the last transformation that the user applied. [CONTINUED IN TEH NEXT COMMENT, SORRY FOR THAT] – gipouf May 27 '13 at 09:59
  • So, my original question was why this demo works, if i will try to explain it in terms of how the canvas is really implemented (with transformations applied to each point in reverse order). I'm getting confused when i try to look at it from this point of view. Do you understand what i mean? :) Or maybe i just have no idea how the canvas works, and i just misled myself totally. **I would be glad to chat with you about it, if it is possible, so we won't go into long long comments. (I gave you an up-vote for now)** – gipouf May 27 '13 at 10:01
  • When you run through the transformations in forward order, they are manipulating the coordinate-space. When run in reverse order, they manipulate your logical coordinate (logical coordinate transformed to device coordinate). Why this works this way is counter-intuitive and is a very deep discussion better discussed with algebra professors. I would just say "it works and go with it". – Matt Houser May 27 '13 at 14:29
1

You seem to be way overthinking transforms!

Here’s the simple rule:

If you apply any set of transforms, then you must undo all of them if you want to get back to your untransformed state.

Period !!!!

So let say you do these 4 transforms:

  • Do #1. context.translate(100,100);
  • Do #2. context.scale(2,2);
  • Do #3. context.translate(-20,50);
  • Do #4. context.scale(10,10);

To get back to your original untransformed state, you must undo in exactly reverse order:

  • Undo #4: context.scale( 0.10, 0.10 ); // since we scaled 10x, we must unscale by 0.10
  • Undo #3: context.translate(20,-50);
  • Undo #2: context.scale( 0.50, 0.50 ); // since we scaled 2x, we must unscale by 0.50
  • Undo #1: context.translate(-100,-100);

Think of it like walking to your friends house.

You turn Right + Left + Right.

Then to go home you must reverse that: Left + Right + Left

You must undo your walking path in exactly the reverse of your original walk.

That’s how transforms work too.

That’s Why !!

markE
  • 102,905
  • 11
  • 164
  • 176
  • Hehe.. I think each of us mean something else for 3 posts in a row... Can we go and chat about it, so i will clarify exactly what i mean? If you can invite me, it will be awesome. Thanks for your effort again. – gipouf May 26 '13 at 10:06