Grabbing a spinning gear

Apprentice
Posts: 10
Joined: 2009.08
Post: #1
My objective is to have a gear that a user can press and spin when they move their finger. When their finger lifts, the gear will spin in the direction they moved their finger. If they grab the gear when it is spinning, I want the gear to stop and follow their finger again. There is several problems in this code but I meainly want to deal with the concepts I am having trouble with.

First I'll go through my code. This is my gear class which extends UIView

Code:
- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
       [self setBackgroundColor:[UIColor clearColor]];
      
       wheel = [[UIImageView alloc] initWithImage:[UIImage imageNamed:kSpinWheelImage]];
       wheel.center = CGPointMake(frame.size.width / 2, frame.size.height / 2);
       [self addSubview:wheel];
      
       rotationTransform = CATransform3DIdentity;
      
       totalDistanceRotated = 0;
    }
    return self;
}

I call this next function when touches begin. First I check to see if the location of the touch is within the image. If it is, I set the lastDegree and return if the touch was in the area of the actual gear (wheel) and not in the empty corners of the image.

Code:
- (BOOL)isPressed:(CGPoint)location {
   if (CGRectContainsPoint([wheel frame], location)) {
      NSInteger XminusA = location.x - [self frame].size.width / 2;
      NSInteger YminusB = location.y - [self frame].size.height / 2;
      NSInteger pointInRelationToCircle = sqrt((XminusA * XminusA) + (YminusB * YminusB));
      NSInteger raduis = [wheel image].size.width / 2 + kTouchPadding;

      lastDegree = [Constants radiansToDegrees:atan2f(location.y - wheel.center.y, location.x - wheel.center.x)];

      return pointInRelationToCircle < raduis;
   } else {
      return NO;
   }
}

Then if it returns true, I set a isGear flag to true in my main view, and call this method on touches moved.

Code:
- (void)rotateWheelByDisplacement:(CGPoint)location {
   CGFloat radians = atan2f(location.y - wheel.center.y, location.x - wheel.center.x);
   CGFloat rotation = [Constants radiansToDegrees:radians];

   totalDistanceRotated += rotation - lastDegree;

   rotationTransform = CATransform3DRotate(rotationTransform, [Constants degreesToRadians:rotation - lastDegree], 0.0, 0.0, 1.0);
   lastDegree = rotation;
  
   wheel.layer.transform = rotationTransform;
}

I am going to spin the gear with the velocity of their finger. So totalDistanceRotated will track how far their finger moves, then when everything is working I will track the time it took for them to move it as well. Then on touches ended, I call

Code:
- (void)animateWheelToStop {
   rotateAnimation = [CABasicAnimation animation];
   rotateAnimation.keyPath = @"transform.rotation.z";
   rotateAnimation.removedOnCompletion = NO;
   rotateAnimation.fillMode = kCAFillModeBoth;
   rotateAnimation.duration = (kSpinSpeed);
   rotateAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
  
   rotationTransform = CATransform3DRotate(rotationTransform, [Constants degreesToRadians:totalDistanceRotated * -2], 0.0, 0.0, 1.0);
   rotateAnimation.toValue = [NSValue valueWithCATransform3D:rotationTransform];
  
   [wheel.layer addAnimation:rotateAnimation forKey:@"rotateAnimation"];

   totalDistanceRotated = 0;
}

I also call removeAllAnimation when the gear is pressed.

Code:
- (void)removeAllAnimations {
   wheel.layer.transform = rotationTransform;
   [[wheel layer] removeAllAnimations];
}

So, there is lot of areas that are buggy and could use improvement, if you want to comment on them, I am all ears. However, the two biggest things I would like help with are...

- How do you grab the gear in the current position when it is spinning? I think the layer animations don't actually change the layer? So I attempted to transform the layer before I remove the animation. This would only work if the animation already finished and not if the animation was still going since I set the rotationTransform to the final destination of the gear. That doesn't work anyways. Even if the gear is stopped, when I click on the gear, it will completely change the rotation of the layer before it follows the finger, which looks good.

- When I look at it on the iPhone, sometimes it can look "jerky" and not completely smooth. I am assuming this is because I transform the layer to the next location of the finger. So depending on how often the touches moves is called, it could jump from position to position. Is there a way to make it look smooth? I tried to animate it to the new position like I am animating it when touches end but I just couldn't get it too look close to right. I tried to make the animation really fast so it could finish before the next touches moved, and removed the animation just encase it was called again. Not even close. I have a feeling I am taking the wrong approach.

Any suggesting or points in a right direction is greatly appreciated.
Quote this message in a reply
Moderator
Posts: 3,579
Joined: 2003.06
Post: #2
Amphro Wrote:- When I look at it on the iPhone, sometimes it can look "jerky" and not completely smooth. I am assuming this is because I transform the layer to the next location of the finger. So depending on how often the touches moves is called, it could jump from position to position. Is there a way to make it look smooth? I tried to animate it to the new position like I am animating it when touches end but I just couldn't get it too look close to right. I tried to make the animation really fast so it could finish before the next touches moved, and removed the animation just encase it was called again. Not even close. I have a feeling I am taking the wrong approach.

Don't know about the other stuff, but you can use a "moving average" to smooth it out a lot. Here's a thread on it a while back which relates.
Quote this message in a reply
DoG
Moderator
Posts: 869
Joined: 2003.01
Post: #3
Your main problem is probably that CoreAnimation indeed only affects the so called "presentation" layer, not the actual layer. You could try getting the transform of the presentation layer, and apply that to the real layer, when the touches end, so at least it'd be consistent. Or you could do like I did, and write a simple CAAnimation replacement that does what I want it to do.
Quote this message in a reply
Apprentice
Posts: 10
Joined: 2009.08
Post: #4
Quote:Don't know about the other stuff, but you can use a "moving average" to smooth it out a lot. Here's a thread on it a while back which relates.

Thats a good idea for the velocity when I animate the gear after touches end. I can't use it to make the wheel go smoother because problem was in touchesMoved. I just added more transformations if the distance from the last touchesMoved was too big.

Quote:You could try getting the transform of the presentation layer

Thats a good idea. I didn't even know about the presentation layer. I tried to set my layer.transform to the presentationLayer.transform I got an incompatible argument error. I didn't think you could do that anyways, unless I am doing it wrong. So I tried to extract a value from it like this.

Code:
CATransform3D temp = CATransform3DIdentity;
CGFloat rot =  [[[wheel.layer presentationLayer] valueForKey:@"transform.rotation.z"] floatValue];
temp = CATransform3DRotate(temp, rot, 0.0, 0.0, 1.0);

Then I would set my actual layers transform to temp. Although it gives me a doesNotRecognizeSelector exception on the second line. Do you know how I would apply the transform to the actual layer.

Quote:Or you could do like I did, and write a simple CAAnimation replacement that does what I want it to do

How would I do that? I couldn't find anything on it. Would I extend CAAnimation, or maybe just write a valueFunction?
Quote this message in a reply
Moderator
Posts: 3,579
Joined: 2003.06
Post: #5
You'd probably need to do something like:

CATransform3D myTransform = [whatever.layer transform];
CATransform3DRotate(myTransform, rot, 0.0, 0.0, 1.0);
[whatever.layer setTransform:myTransform];

Note that it's been quite a while since I've played with CoreAnimation and I'm brain-fried ATM, so I don't know if I got this right. Wink
Quote this message in a reply
Apprentice
Posts: 10
Joined: 2009.08
Post: #6
Yea you got that right and thats what I do to rotate the gear when the finger is still touched. The problem I am having is with the presentationLayer. The function actually returns an id, so you can't do

Code:
CATransform3D myTransform = [[whatever.layer presentationLayer] transform];

I am not sure how to get the value out of a transform either. I tried this too,

Code:
NSValue* zValue =  [[whatever.layer presentationLayer] valueForKey:@"transform.rotation.z"];
[whatever.layer setValue:zValue forKey:@"transform.rotation.z"];

but no luck. When I click on the image, it still goes to where the toValue was set in the animation.
Quote this message in a reply
Moderator
Posts: 3,579
Joined: 2003.06
Post: #7
Just because it's an id, doesn't mean you can't set it. If the id really is a layer, just cast it with something like (CALayer *)myID. If it fails then the returned id wasn't what you thought it was and you need to check your code.

You can't simply get and set a rotation on a transform by trying to directly extract information (e.g. there is no "transform.rotation.z"). The CATransform3D is a 4 x 4 matrix. You'll need to read up on matrices to understand what you're dealing with. Typically you'd store a copy of your rotation value and then apply that to the transform, perhaps after clearing the transform to the identity matrix every time (unless you're accumulating rotations or other transformations).
Quote this message in a reply
Moderator
Posts: 133
Joined: 2008.05
Post: #8
When you have a CALayer, there are actually two of them: the presentation layer and the model layer. The reason it returns id is because you can (and often do) subclass CALayer. You'll need to cast it, [(CALayer *)[layer presentationLayer] doSomething], to make sure the correct method signature is being used. The presentation layer is the current appearance of the layer, the model layer is the layer without any animations applied to it.

When you add an animation object to a layer, that layer begins animating. The presentation layer appears on the screen (and asking it for a property returns the current value), the model layer is removed from the screen and does not change (its properties contain the start values). When the animation ends, the presentation layer reverts back to the model layer.

When you want to change a layer permanently with an animation, you add an animation object to the layer, then directly set the property of the layer. While animations are occurring, the presentation layer is on screen; you can do whatever you want to the layer itself and it won't be visible until the animation ends. Typically, this means you add an animation to a layer and then set that layer's property to the final value of the animation.

When you directly set an implicitly animateable property (frame, bounds, position, transform, opacity) an animation occurs AND the model layer is also updated. You won't need to set the property again and you get the animation for "free".

Transforms are there own thing and have many more applications than just Core Animation. iDevGames member ThemsAllTook has a great tutorial on all things linear algebra related on his website. I suggest you start there. They are not easy concepts, but are vital if you doing any graphics work.
Quote this message in a reply
Apprentice
Posts: 10
Joined: 2009.08
Post: #9
Thanks guys, casting it worked great. Now I can grap the gear mid animation and continue to move it perfectly.

I also used the moving average which works good, thanks AnotherJake. Although the gear animates in the wrong direction when I let go and changing the sign doesn't work. It still spins in the wrong direction. I'll figure that one out though.

Also thanks longjumper for giving me the link to the tutorials. I am defiantly going to read up on those. It has been two years since I took any math like linear algebra and about five since I took trig. So just getting back into the swing of things. Its amazing how fast you lose that when you don't use it. I didn't really feel like I learned matrices really well either, especially transformation. Hopefully all of this and those tutorials will help me in my intro to graphics class this quarter. Super stoked.

Thanks again.
Quote this message in a reply
Apprentice
Posts: 10
Joined: 2009.08
Post: #10
So now I can't get the gear to spin in the correct direction.

If I leave it how I had it, which is

Code:
rotationTransform = CATransform3DRotate(rotationTransform, [Constants degreesToRadians:totalDistanceRotated], 0.0, 0.0, 1.0);
   rotateAnimation.toValue = [NSValue valueWithCATransform3D:rotationTransform];

Then I get this runtime error.

Code:
#0  0x302ac924 in ___TERMINATING_DUE_TO_UNCAUGHT_EXCEPTION___ ()
#1  0x901bbe3b in objc_exception_throw ()
#2  0x302d6ffb in -[NSObject doesNotRecognizeSelector:] ()
#3  0x3026e056 in ___forwarding___ ()
#4  0x3024a0a2 in __forwarding_prep_0___ ()
#5  0x003db234 in -[NSNumber(CAAnimatableValue) CA_interpolateValue:byFraction:] ()
#6  0x003dcb83 in -[CABasicAnimation applyForTime:presentationObject:modelObject:] ()
#7  0x0036ed00 in CALayerGetPresentationLayer ()
#8  0x0000f013 in -[SpinIt removeAllAnimations] (self=0xd28460, _cmd=0x90266550) at /Classes/SpinIt.m:120

Here is the code on line 120.

Code:
- (void)removeAllAnimations {
   [[wheel layer] removeAllAnimations];
   [[wheelShadow layer] removeAllAnimations];
    
   rotationTransform = [(CALayer *)[wheel.layer presentationLayer] transform]; <--line 120
   wheel.layer.transform = rotationTransform;
   wheelShadow.layer.transform = rotationTransform;
}

It does not give me that error if I assign the animation a value instead of a transform.

Code:
rotateAnimation.fromValue =[NSNumber numberWithFloat:[Constants degreesToRadians:totalDistanctRotated]];

But then that doesn't give me the results I want. It just sends the gear spinning in whichever way direction. Any ideas?
Quote this message in a reply
Post Reply