views:

1440

answers:

2

I'm working on an iPhone app with a lot of different gesture inputs that you can do. Currently there is single finger select / drag, two finger scroll, and two finger pinch zoom-in / zoom-out. I want to add in two finger rotation (your fingers rotate a point in between them), but I can't figure out how to get it to work right. All the other gestures were linear so they were only a matter of using the dot or cross product, pretty much.

I'm thinking I've got to store the slope between the previous two points of each finger, and if the angle between the vectors is near 90, then there is the possibility of a rotation. If the next finger movement angle is also near 90, and the direction of the vector on one finger changed positively and changed negatively, then you've got a rotation. The problem is, I need a really clean distinction between this gesture and the other ones - and the above isn't far enough removed.

Any suggestions?

EDIT: Here's how I did it in a vector analysis manner (as opposed to the suggestion below about matching pixels, note that I use my Vector struct in here, you should be able to guess what each function does):

//First, find the vector formed by the first touch's previous and current positions.
struct Vector2f firstChange = getSubtractedVector([theseTouches get:0], [lastTouches get:0]);
//We're going to store whether or not we should scroll.
BOOL scroll = NO;

//If there was only one touch, then we'll scroll no matter what.
if ([theseTouches count] <= 1)
{
    scroll = YES;
}
//Otherwise, we might scroll, scale, or rotate.
else
{
 //In the case of multiple touches, we need to test the slope between the two touches.
 //If they're going in roughly the same direction, we should scroll. If not, zoom.
 struct Vector2f secondChange = getSubtractedVector([theseTouches get:1], [lastTouches get:1]);

 //Get the dot product of the two change vectors.
 float dotChanges = getDotProduct(&firstChange, &secondChange);

 //Get the 2D cross product of the two normalized change vectors.
 struct Vector2f normalFirst = getNormalizedVector(&firstChange);
 struct Vector2f normalSecond = getNormalizedVector(&secondChange);
 float crossChanges = getCrossProduct(&normalFirst, &normalSecond);

 //If the two vectors have a cross product that is less than cosf(30), then we know the angle between them is 30 degrees or less.
 if (fabsf(crossChanges) <= SCROLL_MAX_CROSS && dotChanges > 0)
 {
  scroll = YES;
 }
 //Otherwise, they're in different directions so we should zoom or rotate.
 else
 {
  //Store the vectors represented by the two sets of touches.
  struct Vector2f previousDifference = getSubtractedVector([lastTouches  get:1], [lastTouches  get:0]);
  struct Vector2f currentDifference  = getSubtractedVector([theseTouches get:1], [theseTouches get:0]);

  //Also find the normals of the two vectors.
  struct Vector2f previousNormal = getNormalizedVector(&previousDifference);
  struct Vector2f currentNormal  = getNormalizedVector(&currentDifference );

  //Find the distance between the two previous points and the two current points.
  float previousDistance = getMagnitudeOfVector(&previousDifference);
  float currentDistance  = getMagnitudeOfVector(&currentDifference );

  //Find the angles between the two previous points and the two current points.
  float angleBetween = atan2(previousNormal.y,previousNormal.x) - atan2(currentNormal.y,currentNormal.x);

  //If we had a short change in distance and the angle between touches is a big one, rotate.
  if ( fabsf(previousDistance - currentDistance) <= ROTATE_MIN_DISTANCE && fabsf(angleBetween) >= ROTATE_MAX_ANGLE)
  {
   if (angleBetween > 0)
   {
    printf("Rotate right.\n");
   }
   else
   {
    printf("Rotate left.\n");
   }
  }
  else
  {
   //Get the dot product of the differences of the two points and the two vectors.
   struct Vector2f differenceChange = getSubtracted(&secondChange, &firstChange);
   float dotDifference = getDot(&previousDifference, &differenceChange);
   if (dotDifference > 0)
   {
    printf("Zoom in.\n");
   }
   else
   {
    printf("Zoom out.\n");
   }
  }
 }
}

if (scroll)
{
    prinf("Scroll.\n");
}

You should note that if you're just doing image manipulation or direct rotation / zooming, then the above approach should be fine. However, if you're like me and you're using a gesture to cause something that takes time to load, then it's likely that you'll want to avoid doing the action until that gesture has been activated a few times in a row. The difference between each with my code is still not perfectly separate, so occasionally in a bunch of zooms you'll get a rotation, or vise versa.

+1  A: 

Two fingers, both moving, opposit(ish) directions. What gesture conflicts with this?

Pinch/zoom I guess comes close, but whereas pinch/zoom will start off moving away from a center point (if you trace backwards from each line, your lines will be parallel and close), rotate will initially have parallel lines (tracing backwards) that will be far away from each other and those lines will constantly change slope (while retaining distance).

edit: You know--both of these could be solved with the same algorithm.

Rather than calculating lines, calculate the pixel under each finger. If the fingers move, translate the image so that the two initial pixels are still under the two fingers.

This solves all two-finger actions including scroll.

Two-finger scroll or Zoom might look a little wobbly at times since it will do other operations as well, but this is how the map app seems to work (excluding the rotate which it doesn't have).

Bill K
I like your pixel idea, that would work great with image manipulation. I'm making a game, though, and the rotation gesture rotates the level to a different perspective, which requires some resource loading and the like so it can't be a direct distinction. +1 for the good idea in image cases, though.
Eli
+2  A: 

I've done that before by finding the previous and current distances between the two fingers, and the angle between the previous and current lines. Then I picked some empirical thresholds for that distance delta and angle theta, and that has worked out pretty well for me.

If the distance was greater than my threshold, and the angle was less than my threshold, I scaled the image. Otherwise I rotated it. 2 finger scroll seems easy to distinguish.

BTW in case you are actually storing the values, the touches have previous point values already stored.

CGPoint previousPoint1 = [self scalePoint:[touch1 previousLocationInView:nil]];
CGPoint previousPoint2 = [self scalePoint:[touch2 previousLocationInView:nil]];
CGPoint currentPoint1 = [self scalePoint:[touch1 locationInView:nil]];
CGPoint currentPoint2 = [self scalePoint:[touch2 locationInView:nil]];
kiyoshi
Ah, distance is the missing link. I can't believe I didn't think about that. I was only focusing on angles and direction of vector, but distance really is I think the biggest difference. And I already knew about previous touch (used for zooming), but thanks for pointing it out.
Eli