Silverlight 3 multi-touch development 101 – pt 2 - Zoom

I’m back with the second part of my series about multi-touch in Silverlight 3. This time I am going to tackle zoom, or pinch depending on who is talking. The zoom will be handled by pinching, but I prefer the term zoom for some reason. Go ahead! Flame me in the comments… :)

This part builds a lot on the previous part about Pan. So if you haven’t read it, I suggest you do so before reading any further… Otherwise, here we go!

The math behind the pinch is the simplest thing in this whole series. All we need to do is figure out the change in distance between the two input points and then make this a relative amount that the client can handle. Unfortunately, I need some other math stuff in there before getting to the easy part…

So if we start by looking at the HandleMovement method in the TouchManager. In this method, we have already some code that takes care of getting information needed for the Pan. My code will be added right after that. To work with the changes that occur in the relationship between point 1 and point 2, I have created a helper method on the TrigUtility class. I call the method GetRelativePosition and it takes 2 points as argument and return an instance of RelativePosition. RelativePosition is a simple little type that I have created for the sole purpose of transporting the returned values from the GetRelativePosition method. I could have chosen to use output parameters, but I prefer getting a nice little package back instead of using a bunch of loose variables. The RelativePosition class looks like this

public class RelativePosition
{
public Point Position { get; set; }
public double Distance { get; set; }
public double Angle { get; set; }
}

 

As you can see, the GetRelativePosition method will give me some information about the position of one point in relation to the other. It will give me a Point telling me where the it is positioned relative to the other point. It will also get me the distance between the points, which is what I will use for the zoom gesture. And finally it will give me the angle. The position and angle will be used for the rotate/pinch gesture.

So…how is the GetRelativePosition implemented? Well…here it goes. As I mention in the last post, my math is probably not up to where it should be, but this seems to work.

First I declare the method using the following signature

public static RelativePosition GetRelativePosition(Point point, Point relativeTo)
{
}

Next, I create an instance of the RelativePosition class, set its values and return it. That’s all I have to do. Like this

public static RelativePosition GetRelativePosition(Point point, Point relativeTo)
{
RelativePosition pos = new RelativePosition();
pos.Position = new Point(point.X - relativeTo.X, point.Y - relativeTo.Y);
pos.Distance = GetDistance(pos);
pos.Angle = GetAngle(pos);
return pos;
}

 

The Position calculation is however relatively easy. I just subtract the X and Y coordinates to get the relational Point. Unfortunately as you probably noticed, I am calling off to some helpers for the distance and angle.

The distance isn’t that hard to figure out. Pythagoras told us that a²+b²=c². That means that c (the hypotenuse) equals √(a²+b²). So the GetDistance method looks like this

private static double GetDistance(RelativePosition position)
{
return Math.Sqrt(position.Position.X * position.Position.X + position.Position.Y * position.Position.Y);
}

 

I pass in the RelativePosition object. It had its Position property set in the first part of the GetRelativePosition method. And since that has been set, I can use that for the distance calculation.

The GetAngle method is a little bit trickier. First of all, all trig calculations in .NET is done using Radians. Unfortunately, I’ve been brought up using only degrees. I guess most of us have. So to make my debugging experience easier, I have decided to keep all my calculations in degrees. This causes me to convert back and forth quite a lot, but I’m willing to pay that price. But instead of doing this manually all over the place, I have added another pair of helper methods. They are called ToDegree and ToRadian. The are very simple little methods, but they make the code so much more readable

public static double ToDegree(double radians)
{
return radians * (180.0 / Math.PI);
}
public static double ToRadian(double degrees)
{
return degrees / (180.0 / Math.PI);
}

 

So…let’s get started with the GetAngle method. I start my calculation off by figuring out what quadrant I am working in. The lovely trig method ATan can only handle 0-90 degrees. So I need to make sure that my point stays within that and add some compensation if needed afterwards. So I start by checking if X is less than 0. If that is the case, it is going to be at an angle bigger than 180. If not, it is between 0 and 180. Next, I check whether the points Y is less than 0. If the X is greater than 0 and Y is less than 0, the point is in the first quadrant. If Y is greater than 0, it is in the second quadrant. If X is less than 0 and Y is greater than 0 it is in the third and if not it is in the fourth.

To illustrate what I am trying to explain, I have added a little diagram. Most of you have probably already figured out what I am talking about, but if you haven’t here is the diagram.

Angles

I hope that made sort of sense. So, the blue lines are the angles that I am working with. And I use the red lines together with the ATan function to get the actual angle. And according to my logic above, the “a point” is in the first quadrant, the “b point” in the second, “c” in the third and “d” in the fourth. So to get the “real” angles for those that aren’t in the first quadrant, I need to add a “compensation” value to the angle since I am only calculating the angle within the quadrant. Getting the angle within the quadrant is, as I mentioned before (several times), done by using the ATan function. The value passed in should be the the length of the opposite side divided by the length of the adjacent side. The code looks like this

private static double GetAngle(RelativePosition position)
{
double angle;
if (position.Position.X < 0)
{
if (position.Position.Y < 0)
angle = Math.Atan(-position.Position.Y / -position.Position.X) + ToRadian(270);
else
angle = Math.Atan(-position.Position.X / position.Position.Y) + ToRadian(180);
}
else
{
if (position.Position.Y < 0)
angle = Math.Atan(position.Position.X / -position.Position.Y);
else
angle = Math.Atan(position.Position.Y / position.Position.X) + ToRadian(90);
}
return ToDegree(angle);
}

 

As you can see, all the different scenarios do more or less the same thing, except for the little negative sign in front of some of the values to get the value within the correct scope. And then I add the compensation afterwards…

After that sidestep (with a non mathematician trying to teach math) I am actually done with the GetRelativePosition method. So I can flip back to the HandleMovement method and do the “real” work… As I said before, I start my code right after the code I wrote in the method in the last post.

I start by getting the second points position relative to the first point for both the “before” and “after” points.

RelativePosition lastRelPos = TrigUtility.GetRelativePosition(points[1].LastGlobalPosition, points[0].LastGlobalPosition);
RelativePosition newRelPos = TrigUtility.GetRelativePosition(points[1].NewGlobalPosition, points[0].NewGlobalPosition);

 

As you can see, I work with the global positions. The reason for this is that I will use these points for the rotation as well, which need to be global.

Figuring out the zoom from this is as I said before quite simple. I just have to see how much the distance has changed between the “before” and “after” and then divide that change with the “before” distance to get the relative change that has been made. That calculation will give me the change, but I will add 1 to it. So that an zoom of 20% comes out 1.2 and a de-zoom of 20% yields 0.8. That makes it easier to work with in the end…

Finally, if the zoom isn’t 1.0 (that means unchanged) I raise a Zoom event by calling OnZoom.

double zoom = 1 + (((newRelPos.Distance - lastRelPos.Distance) / lastRelPos.Distance));
if (zoom != 1)
OnZoom(zoom);

 

The OnZoom and the Zoom event is the standard garden variety code you see all the time

protected virtual void OnZoom(double zoom)
{
if (Zoom != null)
Zoom(this, new TouchEventArgs<double>(zoom, GetActivePointsLocal(), GetActivePointsGlobal()));
}
public event EventHandler<TouchEventArgs<double>> Zoom;

 

That’s the TouchManager part of handling the Zoom event. Let's look at the client code for it.

I start off by modifying the TouchDemoView constructor to attach a handler to the Zoom event

public TouchDemoView()
{
InitializeComponent();
_tm = new TouchManager(TranslatedContainer);
_tm.Pan += OnPan;
_tm.Zoom += OnZoom;
}

 

Next, you might assume that I just create a handler that sets the ScaleTransform’s CenterX and CenterY and then set the scale. Unfortunately, it isn’t that easy. Since the user might change the center point for the zoom several times while using the application, this will not work. If you want to, you can try doing it. But it will start flicking the element around the screen. The reason for it is pretty simple, but hard to explain quickly in writing, so I suggest you just trust me that it isn’t a good idea.

Instead, I have to do the scaling from the top left corner of the application and translate the element to make it look as if I am scaling from the center point between my fingers… So let’s look at it

void OnZoom(object sender, TouchEventArgs<double> e)
{
Point zoomPoint = TrigUtility.GetCenterOfPoints(e.LocalPoints[0], e.LocalPoints[1]);

double relativeZoom = e.Payload - 1;
Point zoomTranslation = new Point(-zoomPoint.X * relativeZoom, -zoomPoint.Y * relativeZoom);
...
}

 

As you can see, I start by getting the center point between the two input points. That’s where I want it to look as if the zoom is happening. Next, I figure out how much my point will move from the top left corner during my zoom. And I get that value inversed so that I know how much I need to mover the element to compensate the movement to make it look as if it is zooming from that point… Make sense? If not…here is a small diagram to explain it

Translation

So, X1 and Y1 is the original distances I got before the zoom. X2 and Y2 is the distance after. X3 and Y3 is the change that has happened. And if I get X3 and Y3 and translate the box the same amount in the opposite direction, the center point will stay in the same place, and the zoom will look as if it happened from the point…

So the last part of the event handler looks like this I think… To be honest I made up the code. The final code handles the current rotation and looks completely different. But I will get back to that as I write the third and probably final part of this series…

Scaling.ScaleX *= e.Payload;
Scaling.ScaleY *= e.Payload;
Translation.X += zoomTranslation.X;
Translation.Y += zoomTranslation.Y;

That’s it for today’s part. I have now covered pan and zoom, but the application probably still feels very static and not very impressive. We need to add that final rotation before it starts feeling cool and natural. That will, as I said before, be covered in the next part of this series. I hope I see you then!

Cheers!

Comments are closed