19/9/11

Implement pan/zoom feature with multi-touches screen (Part 1)


(Image source: top9tip.com)

When Apple rolled out it multi-touches device -  the iPhone, they also defined some useful, intuitive gestures for a certain purposes like pinch to zoom, pan to moving around in many of apps like Photos, Maps. These gestures become industry standard for their tasks and even used more and more in other apps like pinch to open/close an article in Flipboard..., pan/zoom in drawing app and game like Happy Farm...
This post will give you some ideas on how to implement your own pan/zoom.


Platform: iOS - OpenGL
OpenGL pre-configuration: Draw an off-screen texture to an onscreen frame buffer EAGLView

Texture ------> onscreen FBO ---> render buffer -> context
We can understand the task easier and get it done by simplifying it into smaller steps:

Step 1: Implement simple and acceptable Zoom/Pan function


Goal
A simple pan/zoom function, you can zoom & pan to any point of the drawing.

Technical

We define a pan by using a vector, and a zoom by a scale number. Let’s store these values in a struct:
typedef struct {
GLfloat x;
GLfloat y;
  GLfloat zoom;
} Transforms;
// Where (x, y) is panning vector, and zoom is zooming scale level.
OpenGL
You can change make our draw bigger with glScalef, and move it by a vector with glTranslatef:.
We need to modify the params of glTranslatef and glScalef when drawing view:
glTranslatef(transforms.x/screenWidth, - transforms.y/screenHeight, 0);  
glScalef(transforms.zoom, transforms.zoom, 1.0);
Multitouches
To glue these internal settings with multitouches gesture:
- In your touchesMoved:withEvent: methods, check if event touches included 1 or 2 touches
For pan: Apply for event with 1 touch (which moved from point A to point A_ in screen), get pan's vector by:
touch1 = [[touches allObjects] objectAtIndex:0];
A = [touch1 previousLocationInView:self];
A_ = [touch1 locationInView:self];
(transform.x, transform.y) = panning vector = vector AA_
For zoom: Apply for event with 2 touches
Similarly, calculating locations for touch2: B & B_
transforms.zoom = zooming scale = A_B_ / AB
Result
Now you have a good enough pan/zoom, you can zoom and go to any point of the drawing.

You also may notice that depend your OpenGL configuration (in iOS or MacOSX) then your view will be always zoomed at center or lower left corner.  A point at a corner after being zoomed will be moved out of the screen.

To make the drawing zoom at a corner:

We first make a zoom at center of screen.
Then drag/perform pan drawing to the corner.

Step 2: Make it better - Implement zooming at any point on drawing


Goal
Open Maps app, when you double click at any point, the map will be zoomed right at that point, even if that point is in a corner How to make a zooming like that?

We are going to build zooming function at a point:
- (void)zoomAtPoint:(CGPoint)point scale:(CGFloat)scale;
This method will modify transform setting appropriately when program receive zoom gestures
A simple thought to solve this is to utilize code in step 1, and add an appropriate pan right after a zoom.

Technical
Remember we have an off-screen texture, then we draw it on an on-screen frame buffer, again this buffer will be rendered to the view. This means we have 3 layers which affect how a drawing is displayed.

In which layer will zooming (the drawing is double in size) actually happen?
In Step 1, we know that transforms.zoom is used for glScalef, and this function will take effect when texture is drawn into onscreen frame buffer. In this process, zooming effect is actually happened


So, why do last time we always zoom on center of the screen?
That is happened when the double drawing is rendered into the view, OpenGLES 3D coordinate system has origin at the center of the screen (while in Mac OSX, this origin is in lower left corner of the screen)

Thus, after we double click on a point in screen, that point is not displayed under our finger in screen anymore, but is moved farther the center of the screen

We need to pan that point from Ao to A (vector AoA), but how to know the coordinate of Ao?
To do this, we need to go deeper into 3 layers of displaying. Let’s name some points:
A_w, Ao_w: coordination of A and Ao in view layer (which is also A and Ao)
A_d, Ao_d: actually coordination of A and Ao in the draw, without any affect display of zoomed/panned drawing
A_t, Ao_t: coordination of A and Ao in the texture, zoom double affect will be performed on A_t point

Follow this sequence of transformation to know coordination of Ao
A_w
--transform/remove pan-zoom effect--> A_d
--transform/convertToGL--> A_t  
--zoom at center--> Ao_t
--transform/convert GL to view--> Ao_d
--apply pan-zoom affect--> Ao_w

Create function for each steps, take into account the difference between coordinate of each layers:
View: origin at lower - left corner
On-screen buffer: origin at center
Texture: origin at top - left corner

This is one of a tough part of implementation, it requires a little knowledge about mathematic and coordinate transform. Try to have unit test and make sure each task done correctly before jump into next task. Below is instruction for first transforms: remove pan-zoom effect

//I. Remove panning
    x -= transforms.x;
    y -= transforms.y;
/*II. Remove zooming
  1. Move from bottom-left to center
    2. scale with current zoom (not new zoom caused by double click)
    3. move from center back to bottom-left */
   x = (x - screenWidth/2)/transforms.zoom + screenWidth/2;
   y = (y - screenHeight/2)/transforms.zoom + screenHeight/2;

You can find the idea for the second here: convertToGL, the forth and fifth is the opposite transform of the first and second one.
When you get all point converting done correct, perform a pan with vector AoA will bring Ao back to under your finger. Eureka!

Step 3: Make it feel cool like Maps app - Integrate multi-touches gestures: pinch to zoom
(to be continued)