Flyingsand
73 posts

#16033
Affine transforms for a movable origin 6 months ago Edited by Flyingsand on Aug. 18, 2018, 5:26 p.m. Reason: Initial post
I'm having some trouble wrapping my head around the transforms I need for a drawing surface. I'm hoping it's not something super obvious that I'm completely missing, but I've been racking my brain since yesterday on this issue and I'm hoping I can get some help here.
I'm working on an iOS app that has a drawing surface (specific knowledge of iOS/Core Graphics isn't necessary as the problem just concerns transforms). It supports paths and images. It also supports panning (effectively infinite in all directions), and zooming (10%  1000%). The origin of the user coordinate space is the upper left corner. This means that whenever you zoom, shapes "pull" or gravitate towards this origin at the top left. This does not result in a great user experience when you're panning around to shapes that are farther away from the origin, you zoom in, and the shapes drift way off screen towards the origin. What I want is for zooming always to focus on the centre of the screen. i.e. No matter how far away you've panned away from the original origin, zooming in/out should always been relative to the current centre of the screen. I have successfully achieved a movable origin where zooming does happen relative to the centre of the screen. However, it adversely affects the speed of panning when zoomed far in or out. e.g. When zoomed far out, panning is very slow, and when zoomed in, panning is too fast. In other words, the speed of panning does not follow the speed of your finger when panning over the screen. Here is a visual of what's happening: Sample 1 Notice that the origin stays at the centre of the screen and that zooming in/out is relative to the origin even when panning away. When zoomed out, you can see how slow panning is. Here is another video that shows a consistent panning speed, but the origin is no longer fixed to the centre of the screen, so zooming in/out is no longer relative to the centre: Sample 2 In short, the second video illustrates correct panning but incorrect zooming, where the first video shows correct zooming but incorrect panning. :/ Here's how I've approached this problem so far:  As the user pans the screen, I need to move the origin of the graphics context to keep it centred in the screen. This offset value is updated whenever panning is in progress with the absolute value of the panning position. (I'll explain the translation matrix shortly.)
 As the user zooms in/out, update the scaling transform accordingly.
 Since I'm applying an offset to the graphics context in order to move the origin to the centre of the screen, I need to compensate for this when drawing actual paths and images so they appear in the right place on the canvas. Hence a separate translation matrix is applied to paths and images to move them "back" to where they should be.
 Finally during drawing, I concatenate the origin offset and scaling matrices together and apply them to the graphics context, which affects all elements drawn to it. All paths will then have the translation matrix applied to it to compensate for the translation of the graphics context.
To me it seems like it's a problem with the translation matrix that I'm applying to individual paths/images. I've attempted to scale it according to the zoom scale as you can see in the function above, but it obviously isn't right. What am I missing here? Or am I approaching this totally in the wrong way? 
mrmixer
Simon Anciaux
534 posts

#16058
Affine transforms for a movable origin 6 months ago
I tried for a few hours to get some code doing what you want but I failed. I feel like it should be as simple as translate, scale, translate but can't seem to make that work. I got something that resemble what you want but somehow the zoom doesn't zoom at the center of the screen. I'll try to continue when I've got some time as this interests me and bothers me that I can't solve it.
My idea was that before you start to zoom, you compute the "world space" position of the "screen space" center. You compute the new zoom factor. And then you compute the world space position of the screen space center with the new zoom value. The difference between the two would then be the world space offset, that you need to scale with the current zoom factor, needed to keep the same point at the center. Here is the code, but it's not quite working (zoom doesn't go correctly towards the center). The pan works great independently of the zoom level.

mmozeiko
Mārtiņš Možeiko
1880 posts
/ 1 project

#16060
Affine transforms for a movable origin 6 months ago Edited by Mārtiņš Možeiko on Aug. 20, 2018, 10:24 p.m.
I feel like expressing this is matrices will be much simpler than manually manipulating various variables.
Basically when you want to zoom, you need to adjust current position. With matrices this looks like this:
This will adjust posx/posy panning offset in a way that zoom happens around centerx/centery point. Here is code that demonstrates this. It is based on SDL2, but I'm sure it should be pretty easy to adjust it to any API  it simply draws bunch of random lines by transforming them with 2D matrix: https://gist.github.com/mmozeiko/c9ecb8b206c245198f0a5aedc21f5a64 Code allows you to set any point as "center" point for zooming  for example, mouse position would be an good choice. Here's a gif that shows how it looks at runtime  red rectangle is window center around which "zooming" happens. Pan with left mouse button, zoom with wheel. 
Flyingsand
73 posts

#16077
Affine transforms for a movable origin 5 months, 4 weeks ago Edited by Flyingsand on Aug. 22, 2018, 8:19 p.m. Reason: Adding code sample
Thanks for the code sample and tips, mmozeiko! I'm closer (really close actually), but I'm still having a couple of issues. On the good side, I fixed the panning consistency when going between zooming and panning based on your feedback.
Similar to my original case, fixing any one of these two issues, breaks the other one.. So unbelievably frustrating! Here are the two cases and the relating issue: 1) I can make zooming focus in on any arbitrary point, which feels really nice from a user perspective. Panning works fine as well. However, any subsequent zooming after the initial one causes the paths/strokes to "jump". Here's a video for reference: Sample 1 2) By fixing the jumping issue above, zooming no longer focuses in on the focus point. Instead it kind of "drifts" around it. It's pretty close much of the time, but not good enough for my tastes. Here is a video reference: Sample 2 Based on your feedback, I made some fundamental changes to how I apply the transform. I no longer apply any transformation to the graphics context (canvas) itself. Instead, the transform is applied onto the paths themselves. Secondly, I handle zooming and panning in separate cases as you will see in my code sample. When just a panning gesture is recognized, it just translates the affine transform by a delta amount. Here is the code inside the draw method of my canvas view:
You can see each case around the #if #else #endif blocks. In the first case (zoom in on focus point), the affine transform is created new each time, which is probably the cause of why the paths jump when initiating subsequent zooms. The new affine transform doesn't match the previous one from when the last zoom operation finished. So I must be missing an offset value here in order to create the new affine transform that "picks up" from the last one. In the second case (zooming focus drifts, but no jumps), the affine transform is not recreated each redraw, as everything is based on deltas. I suspect this is the cause of the drifting  the deltas are slightly off, or not being applied quite right in order to focus in on the right point. Any further insight into my issue here? I'm quite surprised how fussy this has turned out to be. It seems like it should be relatively straightforward (and quite a common) thing to do when dealing with a drawing surface. Edit: In case it's useful, here is the code that handles the iOS gestures themselves:

mmozeiko
Mārtiņš Možeiko
1880 posts
/ 1 project

#16078
Affine transforms for a movable origin 5 months, 4 weeks ago Edited by Mārtiņš Možeiko on Aug. 22, 2018, 8:40 p.m.
Sorry, I'm not familiar with these iOS api functions. Check carefully if your calculations match. Applying transform to graphics context should work fine. That's pretty much what I am doing in my code example. It applies one transformation matrix to whole drawing context. And it allows to specify arbitrary point to zoom around, for example, mouse cursor.
I don't think your "drifting" is related to matching previous transform/deltas. In my example I am also creating transformation matrix on every frame from scratch. I believe your calculation of how to apply scaling is wrong, it scales around different point than you expect. That's why it seems it is moving away. Check your math. 
Flyingsand
73 posts

#16080
Affine transforms for a movable origin 5 months, 3 weeks ago mmozeiko Yep, I was applying my scaling slightly wrong. I carefully went through the math again, and got it working! It feels great! The only additional thing I needed to do was to account for simultaneous pan & pinch gestures in the calculation of the transform. Thanks again for your help. 
mrmixer
Simon Anciaux
534 posts

#16085
Affine transforms for a movable origin 5 months, 3 weeks ago
@mmozeiko I still can't get this working. I compared your matrix functions and mine and found a difference.
I'm using 4x4 matrices but since I don't use z, let's say they are 3x3 matrices. You use 3x2. When I pass a vector for transformation by a 3x3 matrix I pass vec3(x,y,1). I also use column major matrix (first 3 elements of the array are the first column of the matrix). I thought those changes made no difference but after comparing the result of your transformation there was a difference with the scaling function. You only multiply the m[0] and m[3] for the scale, but shouldn't the translation part (m[4] and m[5]) be modified too ? Lets say I translate the identity matrix 2 on x and 2 on y, and than multiply it by a scale matrix of 2 on x and y. What does the scale matrix looks like ? Is it this ?
If it's the case the result is different than what you have in code that produces
If the above is correct and the code does what you want, why do we leave the translation part out of the scale multiply ? 
Flyingsand
73 posts

#16086
Affine transforms for a movable origin 5 months, 3 weeks ago mrmixer It's not that the translation is left out, it's that the order is wrong. So going through the example you gave, translating the identity matrix by (2, 2), we have:
Then scaling that result by (2, 2) is:
i.e. Matrix multiplication is not commutative. 
mrmixer
Simon Anciaux
534 posts

#16106
Affine transforms for a movable origin 5 months, 3 weeks ago
I knew the order is important but still I messed it up. Thanks
