Zooming smoothly

BlitzMax Forums/BlitzMax Programming/Zooming smoothly

TomToad(Posted 2008) [#1]
I am trying to smoothly zoom into a portion of the screen over a given number of frames. For example, suppose the screen is 800x600 and I want to zoom in so that 150,150 to 170,170 now fills the screen, but I want it to zoom in gradually over several frames.
Problem is, if I do it linearly, it will appear to speed up as I get closer to the portion of the screen I'm zooming into. I also tried using a sine function to taper off the zoom, which made things a little better, but was still getting a speedup toward the end.
Here is a sample zooming in on the x on the screen using both linear zooming and sine zooming. As you can see, both versions visually speed up toward the end and isn't consistent across the entire zoom range.



Warpy(Posted 2008) [#2]
OK, I've stared at it for a bit, and I think I can see what you're doing.
If you consider the part of the world that is drawn on the screen as a box in space, what you're doing is shrinking the box (linearly!) down until it fits the square (150,150)->(170,170).

But then to draw the screen, you convert the co-ordinates of your points using a function that isn't linear! If you take the line
Local rx1:Int = (x1-ZX1)*800/(ZX2-ZX1)

and expand it out, the bit depends on t (the time, starting at t=0 and ending at t=1) behaves like 1/t, which is where you're getting the acceleration from.

So, we need some new thinking.
We want to stretch a rectangle with top-left corner (x1,y1) and bottom-right corner (x2,y2) so that every point in the rectangle moves towards its final position at a constant speed.
Importantly, (x1,y1) and (x2,y2) must not move in the virtual space, just when they are projected onto the screen.
So we will make a function Zoom(x,y,t) that, given a point (x,y) in the virtual space and the time t, returns a point (px,py) on the screen at which to draw that point.
Minor niggle here that it's difficult to return two things at once in BMax, so we'll actually need two functions, ZoomX(x,t) and ZoomY(y,t).

We need ZoomX(x1,0) = x1 and ZoomX(x1,1) = 0. We also need ZoomX(x2,0)=x2 and ZoomX(x2,1)=800.

A note about linear interpolation, which I think you understand because you had it half-right in your code, but I might as well make it explicit:
To interpolate a value X linearly between two values A and B, so that at time t=0, X = A ,and at time t=1, X = B, set
X = A * (1 - t) + B * t


So, try:
Function ZoomX:Double(x:Double,t:Double,x1:Double,x2:Double)
	Local zx1:Double=x1*(1-t)	'this is the position of x1 on the screen at time t.
						'note that this is linear, and has zx1(t=0) = x1 and zx1(t=1) = 0
						'we could also work out zx2, for the point x2, but we don't need to.
					
	Local width:Double=(x2-x1)*(1-t)+800*t	'this gives us how wide, in pixels, the rectangle is 
										'on the screen at time t. Note that it is linear and 
										'we have width(t=0) = x2 - x1 and width(t=1) = 800.
								
	Local nx:Double=(x-x1)/(x2-x1)	'We want to know where the point x is in the rectangle as
								'a proportion of the width. Note that this number does not
								'depend on t, so remains constant throughout.
	
	Local px:Double=zx1+nx*width		'finally, this gives us the position on the screen of our point.
								'Note that if x=x1 then nx=0 so px = zx1 as expected, and if
								'x=x2 then nx=1 so we will get px = x2*(1-t)+800*t = zx2.
								
	Return px
	
End Function


The function for ZoomY looks much the same, but with Ys instead of Xs and 600 instead of 800.

Now, we can rewrite the LinearZoom function and put it back in the program:
SuperStrict

Graphics 800,600

While Not KeyHit(KEY_ESCAPE) And Not AppTerminate()
	Cls
	DrawLine 150,150,170,170
	DrawLine 150,170,170,150
	DrawText "Press L for linear zoom",10,500
	Flip
	If KeyHit(KEY_L) Then LinearZoom(150,150,170,170)
Wend


Function ZoomX:Double(x:Double,t:Double,x1:Double,x2:Double)
	Local zx1:Double=x1*(1-t)	'this is the position of x1 on the screen at time t.
						'note that this is linear, and has zx1(t=0) = x1 and zx1(t=1) = 0
						'we could also work out zx2, for the point x2, but we don't need to.
					
	Local width:Double=(x2-x1)*(1-t)+800*t	'this gives us how wide, in pixels, the rectangle is 
										'on the screen at time t. Note that it is linear and 
										'we have width(t=0) = x2 - x1 and width(t=1) = 800.
								
	Local nx:Double=(x-x1)/(x2-x1)	'We want to know where the point x is in the rectangle as
								'a proportion of the width. Note that this number does not
								'depend on t, so remains constant throughout.
	
	Local px:Double=zx1+nx*width		'finally, this gives us the position on the screen of our point.
								'Note that if x=x1 then nx=0 so px = zx1 as expected, and if
								'x=x2 then nx=1 so we will get px = x2*(1-t)+800*t = zx2.
								
	Return px
	
End Function

Function ZoomY:Double(y:Double,t:Double,y1:Double,y2:Double)
	Local zy1:Double=y1*(1-t)	'this is the position of y1 on the screen at time t.
						'note that this is linear, and has zy1(t=0) = y1 and zy1(t=1) = 0
						'we could also work out zy2, for the point y2, but we don't need to.
					
	Local width:Double=(y2-y1)*(1-t)+600*t	'this gives us how wide, in pixels, the rectangle is 
										'on the screen at time t. Note that it is linear and 
										'we have width(t=0) = y2 - y1 and width(t=1) = 600.
								
	Local ny:Double=(y-y1)/(y2-y1)	'We want to know where the point y is in the rectangle as
								'a proportion of the width. Note that this number does not
								'depend on t, so remains constant throughout.
	
	Local py:Double=zy1+ny*width		'finally, this gives us the position on the screen of our point.
								'Note that if y=y1 then ny=0 so py = zy1 as expected, and if
								'y=y2 then ny=1 so we will get py = y2*(1-t)+600*t = zy2.
								
	Return py
	
End Function


Function LinearZoom(ex1:Double,ey1:Double,ex2:Double,ey2:Double)
	For Local i:Int = 0 To 300
		Local t:Double = Double(i)/Double(300)
		Line(150,150,170,170,t,ex1,ey1,ex2,ey2)
		Line(150,170,170,150,t,ex1,ey1,ex2,ey2)
		Flip
		Cls
	Next
	WaitKey()
End Function

Function Line(x1:Double,y1:Double,x2:Double,y2:Double,t:Double,ex1:Double,ey1:Double,ex2:Double,ey2:Double)
	Local zx1:Double=ZoomX(x1,t,ex1,ex2)
	Local zy1:Double=ZoomY(y1,t,ey1,ey2)
	Local zx2:Double=ZoomX(x2,t,ex1,ex2)
	Local zy2:Double=ZoomY(y2,t,ey1,ey2)


	DrawLine zx1,zy1,zx2,zy2
End Function


Note that I pass the ex1,ey1, etc. around all the time because I didn't want to confuse matters with new globals, but if you're always doing the same zoom you could just make them globals and set them at the start of the process.


TomToad(Posted 2008) [#3]
Thanks warpy, this will be a big help. I will probably come up with a couple more questions, but right now, time to work for an actual paycheck :)


TomToad(Posted 2008) [#4]
Ok, I had some time to mess with your code and found it doesn't work. It looks fine when only the X is being drawn, but when you're zooming in with all kinds of things drawn around the outside, you notice a definite deceleration.

i thought about it for a while and realized something. If you stretch the world (or shrink the viewport) so that only 90% of the original shows after the first iteration, then on the second iteration, to appear a consistent speed, 90% of the screen from the result of the first iteration needs to be showing.
So instead of decreasing the original viewport linearly 90%, 80%, 70% etc. You need to decrease it exponentially. 90%, 81%, 72.9% etc...
After a bit of thinking, I finally came up with a formula.
X(i)=a*(p^i)
X(i) is the point after i iterations. a is the original position of the point. p is a scaler.
So the idea is, in order to move from a to b in n number of iterations, you need to calculate the value of p in such a way that when i=1 then X(i) = a and when i=n then X(i) = b.
Here's the nifty little formula for that.
p = (b/a)^(1/n)
There's only a few restrictions. One is that the center of the rectangle you are zooming into must be translated to the center of the world before the calculations are performed, which means everything must be translated back afterwards.
Another is that b cannot be 0. That can only happen if the rectangle you are zooming into has either 0 width or 0 height.

Here is an example of zooming with the above method. I have drawn a bunch of polygons around the screen to give a little better perspective of what's going on. On part of the screen is a bunch of concentric circles which marks the bullseye of where I want to zoom into.
As you can see, the zooming appears very consistent.