4

Okay, so I'm trying to make a 'Wheel of Fortune' type of effect with a wheel shape in iOS, where I can grab and spin a wheel. I can currently drag and spin the wheel around to my heart's content, but upon releasing my finger, it stops dead. I need to apply some momentum or inertia to it, to simulate the wheel spinning down naturally.

I've got the velocity calculation in place, so when I lift my finger up I NSLog out a velocity (between a minimum of 1 and a maximum of 100), which ranges from anywhere between 1 and over 1800 (at my hardest flick!), now I'm just trying to establish how I would go about converting that velocity into an actual rotation to apply to the object, and how I'd go about slowing it down over time.

My initial thoughts were something like: begin rotating full circles on a loop at the same speed as the velocity that was given, then on each subsequent rotation, slow the speed by some small percentage. This should give the effect that a harder spin goes faster and takes longer to slow down.

I'm no mathematician, so my approach may be wrong, but if anybody has any tips on how I could get this to work, at least in a basic state, I'd be really grateful. There's a really helpful answer here: iPhone add inertia/momentum physics to animate "wheel of fortune" like rotating control, but it's more theoretical and lacking in practical information on how exactly to apply the calculated velocity to the object, etc. I'm thinking I'll need some animation help here, too.

EDIT: I'm also going to need to work out if they were dragging the wheel clockwise or anti-clockwise.

Many thanks!

Community
  • 1
  • 1
Luke
  • 9,512
  • 15
  • 82
  • 146

2 Answers2

1

I have written something analogous for my program Bit, but my case I think is a bit more complex because I rotate in 3D: https://itunes.apple.com/ua/app/bit/id366236469?mt=8

Basically what I do is I set up an NSTimer that calls some method regularly. I just take the direction and speed to create a rotation matrix (as I said, 3D is a bit nastier :P ), and I multiply the speed with some number smaller than 1 so it goes down. The reason for multiplying instead of subtracting is that you don't want the object to rotate twice as long if the spin from the user is twice as hard since that becomes annoying to wait on I find.

As for figuring out which direction the wheel is spinning, just store that in the touchesEnded:withEvent: method where you have all the information. Since you say you already have the tracking working as long as the user has the finger down this should hopefully be obvious.

What I have in 3D is something like:

// MyView.h
@interface MyView : UIView {
    NSTimer *animationTimer;
}
- (void) startAnimation;
@end


// MyAppDelegate.h
@implementation MyAppDelegate

- (void) applicationDidFinishLaunching:(UIApplication *)application {
    [myView startAnimation];
}

@end

// MyView.m
GLfloat rotationMomentum = 0;
GLfloat rotationDeltaX  = 0.0f;
GLfloat rotationDeltaY  = 0.0f;

@implementation MyView
- (void)startAnimation {
    animationTimer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)((1.0 / 60.0) * animationFrameInterval) target:self selector:@selector(drawView:) userInfo:nil repeats:TRUE];
}

- (void) drawView:(id)sender {
    addRotationByDegree(rotationMomentum);
    rotationMomentum /= 1.05;
    if (rotationMomentum < 0.1)
        rotationMomentum = 0.1; // never stop rotating completely
    [renderer render];
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch *aTouch = [touches anyObject];
    CGPoint loc = [aTouch locationInView:self];
    CGPoint prevloc = [aTouch previousLocationInView:self];

    rotationDeltaX = loc.x - prevloc.x;
    rotationDeltaY = loc.y - prevloc.y;

    GLfloat distance = sqrt(rotationDeltaX*rotationDeltaX+rotationDeltaY*rotationDeltaY)/4;
    rotationMomentum = distance;
    addRotationByDegree(distance);

    self->moved = TRUE;
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
{
}

I've left out the addRotationByDegree function but what it does is that it uses the global variables rotationDeltaX and rotationDeltaY and applies a rotational matrix to an already stored matrix and then saves the result. In your example you probably want something much simpler, like (I'm assuming now that only movements in the X direction spin the wheel):

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch *aTouch = [touches anyObject];
    CGPoint loc = [aTouch locationInView:self];
    CGPoint prevloc = [aTouch previousLocationInView:self];

    GLfloat distance = loc.x - prevloc.x;
    rotationMomentum = distance;
    addRotationByDegree(distance);

    self->moved = TRUE;
}

void addRotationByDegree(distance) {
    angleOfWheel += distance; // probably need to divide the number with something reasonable here to have the spin be nicer
}
boxed
  • 3,895
  • 2
  • 24
  • 26
  • That's a fun little app! I'll be sure to consult it nect time I'm deciding whether I want a bath or a shower, or something :) This does indeed look like exactly what I need (though in 3D). I'm sorry to be a pain – could you give me some more specific details? I'm not too sure what a rotation matrix is and I'm not an algebra expert by any means. – Luke Feb 15 '13 at 16:52
  • I'm struggling to understand what rotation I'd apply to the object in the first place. For example, I don't want to tell it to rotate a specific number of radians/degrees, I want it to rotate infinitely, then I can do as you suggest and try to slow it down with each subsequent spin. How do I begin that process, with the initial spin? All I have is my single floating value representing velocity. – Luke Feb 15 '13 at 16:53
  • "I want it to rotate infinitely" Do you ? – Vinzzz Feb 18 '13 at 09:26
  • Initially, yes. Or at least rotate perpetually whilst slowing down. – Luke Feb 18 '13 at 12:14
  • You keep a float variable somewhere where you store the current rotation (the angle by which you rotate the image of the wheel when rendering). Each time your timer gets called you add to that number some multiple of the velocity and decrease the velocity slightly. Negative velocity is rotation in the other direction. Since it's a repeating timer you'll get it calling you back for the entire lifetime of your program. – boxed Feb 18 '13 at 12:47
  • Oh, and you probably want to just do what I described above if the user isn't in the middle of a touch event. – boxed Feb 18 '13 at 12:49
  • If the user touches it again, it needs to stop immediately and cancel all other rotation acting upon it, I think. – Luke Feb 19 '13 at 16:39
  • Just set the rotation variable to 0 in the beginTouch:... handler. Is this helping or do you need some more specific example code or something? – boxed Feb 20 '13 at 20:09
  • Example code would be great if you could provide some, it's just the conceptual stuff that's really throwing me off. I'm concerned that I've bitten off more than I can chew and I'm still having a little difficulty transitioning from the thoughts here into actually tapping code out. – Luke Feb 21 '13 at 13:40
  • I've edited my answer to include some more concrete code. It's a mix of what I have in the Bit app and modifications for your case. – boxed Feb 25 '13 at 08:50
1

It's going to be a rough answer as I don't have any detailed example at hand.

If you have the velocity when you lift your finger already then it should not be hard. The velocity you have is at pixels per second or something like that. First you need to convert that linear speed to an angular speed. That can be done by knowing the perimeter of the circle 2*PI*radius and then do 2*PI/perimeter*velocity to get the angular speed in radians per second.

If your wheel didn't have any friction in its axis it would run forever at that speed. Well, you can just arbitrate a value for this friction, which is an acceleration and can be represented at pixels per second squared or radians per second squared for an angular acceleration. Then it's just a matter of dividing the angular speed by this angular acceleration and you get the time until it stops.

With the animation time you can use the equation finalAngle = initialAngle + angularSpeed*animationTime - angularAcceleration/2*animationTime*animationTime to get the final angle your wheel is going to be at the end of the animation. Then just do an animation on the transformation and rotate it by that angle for the time you got and say that your animation should ease out.

This should look realistic enough. If not you'll need to give an animation path for the rotation property of your wheel based on some samples from the equation from above.

Fábio Oliveira
  • 2,346
  • 21
  • 30