13

I'm trying to implement a flip animation to be used in board game like iPhone-application. The animation is supposed to look like a game piece that rotates and changes to the color of its back (kind of like an Reversi piece). I've managed to create an animation that flips the piece around its orthogonal axis, but when I try to flip it around a diagonal axis by changing the rotation around the z-axis the actual image also gets rotated (not surprisingly). Instead I would like to rotate the image "as is" around a diagonal axis.

I have tried to change layer.sublayerTransform but with no success.

Here is my current implementation. It works by doing a trick to resolve the issue of getting a mirrored image at the end of the animation. The solution is to not actually rotate the layer 180 degrees, instead it rotates it 90 degrees, changes image and then rotates it back.

Final version: Based on Lorenzos suggestion to create a discrete keyed animation and calculate the transformation matrix for each frame. This version instead tries to estimate number of "guiding" frames needed based on the layer size and then uses a linear keyed animation. This version rotates with a arbitrary angle so to rotate around diagonal line use a 45 degree angle.

Example usage:

[someclass flipLayer:layer image:image angle:M_PI/4]

Implementation:

- (void)animationDidStop:(CAAnimationGroup *)animation
                finished:(BOOL)finished {
  CALayer *layer = [animation valueForKey:@"layer"];

  if([[animation valueForKey:@"name"] isEqual:@"fadeAnimation"]) {
    /* code for another animation */
  } else if([[animation valueForKey:@"name"] isEqual:@"flipAnimation"]) {
    layer.contents = [animation valueForKey:@"image"];
  }

  [layer removeAllAnimations];
}

- (void)flipLayer:(CALayer *)layer
            image:(CGImageRef)image
            angle:(float)angle {
  const float duration = 0.5f;

  CAKeyframeAnimation *rotate = [CAKeyframeAnimation
                                 animationWithKeyPath:@"transform"];
  NSMutableArray *values = [[[NSMutableArray alloc] init] autorelease];
  NSMutableArray *times = [[[NSMutableArray alloc] init] autorelease];
  /* bigger layers need more "guiding" values */
  int frames = MAX(layer.bounds.size.width, layer.bounds.size.height) / 2;
  int i;
  for (i = 0; i < frames; i++) {
    /* create a scale value going from 1.0 to 0.1 to 1.0 */
    float scale = MAX(fabs((float)(frames-i*2)/(frames - 1)), 0.1);

    CGAffineTransform t1, t2, t3;
    t1 = CGAffineTransformMakeRotation(angle);
    t2 = CGAffineTransformScale(t1, scale, 1.0f);
    t3 = CGAffineTransformRotate(t2, -angle);
    CATransform3D trans = CATransform3DMakeAffineTransform(t3);

    [values addObject:[NSValue valueWithCATransform3D:trans]];
    [times addObject:[NSNumber numberWithFloat:(float)i/(frames - 1)]];
  }
  rotate.values = values;
  rotate.keyTimes = times;
  rotate.duration = duration;
  rotate.calculationMode = kCAAnimationLinear;

  CAKeyframeAnimation *replace = [CAKeyframeAnimation
                                  animationWithKeyPath:@"contents"];
  replace.duration = duration / 2;
  replace.beginTime = duration / 2;
  replace.values = [NSArray arrayWithObjects:(id)image, nil];
  replace.keyTimes = [NSArray arrayWithObjects:
                      [NSNumber numberWithDouble:0.0f], nil];
  replace.calculationMode = kCAAnimationDiscrete;

  CAAnimationGroup *group = [CAAnimationGroup animation];
  group.duration = duration;
  group.timingFunction = [CAMediaTimingFunction
                          functionWithName:kCAMediaTimingFunctionLinear];
  group.animations = [NSArray arrayWithObjects:rotate, replace, nil];
  group.delegate = self;
  group.removedOnCompletion = NO;
  group.fillMode = kCAFillModeForwards;
  [group setValue:@"flipAnimation" forKey:@"name"];
  [group setValue:layer forKey:@"layer"];
  [group setValue:(id)image forKey:@"image"];

  [layer addAnimation:group forKey:nil];
}

Original code:

+ (void)flipLayer:(CALayer *)layer
          toImage:(CGImageRef)image
        withAngle:(double)angle {
  const float duration = 0.5f;

  CAKeyframeAnimation *diag = [CAKeyframeAnimation
                               animationWithKeyPath:@"transform.rotation.z"];
  diag.duration = duration;
  diag.values = [NSArray arrayWithObjects:
                 [NSNumber numberWithDouble:angle],
                 [NSNumber numberWithDouble:0.0f],
                 nil];
  diag.keyTimes = [NSArray arrayWithObjects:
                   [NSNumber numberWithDouble:0.0f],
                   [NSNumber numberWithDouble:1.0f],
                   nil];
  diag.calculationMode = kCAAnimationDiscrete;

  CAKeyframeAnimation *flip = [CAKeyframeAnimation
                               animationWithKeyPath:@"transform.rotation.y"];
  flip.duration = duration;
  flip.values = [NSArray arrayWithObjects:
                 [NSNumber numberWithDouble:0.0f],
                 [NSNumber numberWithDouble:M_PI / 2],
                 [NSNumber numberWithDouble:0.0f],
                 nil];
  flip.keyTimes = [NSArray arrayWithObjects:
                   [NSNumber numberWithDouble:0.0f],
                   [NSNumber numberWithDouble:0.5f],
                   [NSNumber numberWithDouble:1.0f],
                   nil];
  flip.calculationMode = kCAAnimationLinear;

  CAKeyframeAnimation *replace = [CAKeyframeAnimation
                                  animationWithKeyPath:@"contents"];
  replace.duration = duration / 2;
  replace.beginTime = duration / 2;
  replace.values = [NSArray arrayWithObjects:(id)image, nil];
  replace.keyTimes = [NSArray arrayWithObjects:
                      [NSNumber numberWithDouble:0.0f], nil];
  replace.calculationMode = kCAAnimationDiscrete;

  CAAnimationGroup *group = [CAAnimationGroup animation];
  group.removedOnCompletion = NO;
  group.duration = duration;
  group.timingFunction = [CAMediaTimingFunction
                          functionWithName:kCAMediaTimingFunctionLinear];
  group.animations = [NSArray arrayWithObjects:diag, flip, replace, nil];
  group.fillMode = kCAFillModeForwards;

  [layer addAnimation:group forKey:nil];
}
Mattias Wadman
  • 11,172
  • 2
  • 42
  • 57

3 Answers3

6

you can fake it this way: create an affine transform that collapse the layer along it's diagonal:

A-----B           B
|     |          /
|     |   ->   A&D
|     |        /
C-----D       C

change the image, and trasform the CALayer back in another animation. This will create the illusion of the layer rotating around its diagonal.

the matrix for that should be if I remember math correctly:

0.5 0.5 0
0.5 0.5 0
0   0   1

Update: ok, CA doen't really likes to use degenerate transforms, but you can approximate it this way:

CGAffineTransform t1 = CGAffineTransformMakeRotation(M_PI/4.0f);
CGAffineTransform t2 = CGAffineTransformScale(t1, 0.001f, 1.0f);
CGAffineTransform t3 = CGAffineTransformRotate(t2,-M_PI/4.0f);  

in my tests on the simulator there still was a problem because the rotations happens faster than te translation so with a solid black square the effect was a bit weird. I suppose that if you have a centered sprite with transparent area around it the effect will be close to what expected. You can then tweak the value of the t3 matrix to see if you get a more appealing result.

after more research, it appears that one should animate it's own transition via keyframes to obtaim the maximum control of the transition itself. say you were to display this animation in a second, you should make ten matrix to be shown at each tenth of a second withouot interpolation using kCAAnimationDiscrete; those matrix can be generated via the code below:

CGAffineTransform t1 = CGAffineTransformMakeRotation(M_PI/4.0f);
CGAffineTransform t2 = CGAffineTransformScale(t1, animationStepValue, 1.0f);
CGAffineTransform t3 = CGAffineTransformRotate(t2,-M_PI/4.0f);  

where animationStepValue for ech of the keyFrame is taken from this progression:

{1 0.7 0.5 0.3 0.1 0.3 0.5 0.7 1}

that is: you're generating ten different transformation matrix (actually 9), pushing them as keyframes to be shown at each tenth of a second, and then using the "don't interpolate" parameter. you can tweak the animation number for balancing smoothness and performance*

*sorry for possible errors, this last part was written without a spellchecker.

Lorenzo Boccaccia
  • 6,041
  • 2
  • 19
  • 29
  • Thanks for your answer. Is the ASCII-art suppose to show a shear transformation? if I try to animate from identity, then to your matrix and them back to identity i get no image at all :( I looked up how to make a shear matrix and it look like this: {{1, sheary, 0}, {shearx, 1, 0}, {0, 0, 1}} If I use that in the animation it seams to work a bit but i need to shear quite much and then the image gets scaled up quite a bit too. – Mattias Wadman Mar 18 '10 at 16:33
  • no shear will make a parallelogram, here we're trying to get a degenerated rhombus, with the A&D corner moving towards the center and not the A&B corners moving toward left I'll have to try this on the device tomorrow to get the actual matrix parameters – Lorenzo Boccaccia Mar 18 '10 at 19:00
  • Ok. I have been reading how affine transformations actually work and the transformation you describe with the diagram should as you say be affine, preserving points on a line and ratios (as long as A and D are "opposites" i guess?). But I havent really figured how to think about the numbers in the matrix. – Mattias Wadman Mar 18 '10 at 19:37
  • in term of coumpound standard transformation, it's a 45 degree rotation counterclockwise, then a scaling along the horizontal axis (which collapses the a&c vertex on the center and then a 45 degree rotatation clockwise - as I said, that should be the resulting matrix, but I'll try it and post some actual code (I'm just updating there with the theory behind) – Lorenzo Boccaccia Mar 18 '10 at 22:12
  • I was fiddling around with very similar code yesterday after you commented on the compound transformations, but the animation behaves very strange. Maybe core animation does something weird when calculating values for a smooth transition? – Mattias Wadman Mar 19 '10 at 14:50
  • Do you mean tweak t2, scaling, or tweak the whole t3 transform even more? – Mattias Wadman Mar 19 '10 at 14:51
  • It's because it doesn't make the transition interpolating between the initial and final position of each 'corner' of the layer but works by creating a linear interpolation for each value within the transform matrix from the initial to the target value. – Lorenzo Boccaccia Mar 19 '10 at 16:21
  • Yes I meant that the transformation does not work as one expect when just interpolating between the values of the initial and target matrix. Is that what you meant, that it does not work to just interpolate like core animation does? – Mattias Wadman Mar 19 '10 at 23:31
  • Thanks for your time and patience! not that i really need it, but how would a version that rotate around a arbitrary angle look like? :) – Mattias Wadman Mar 20 '10 at 01:38
  • Arbitrary angle seams to work fine, but I do not fully understand why – Mattias Wadman Mar 21 '10 at 15:24
  • don't really know. one thing may be that the more near you are to a singular matrix, the worse the interpolation of core animation will be. another is that the more the rotation, the worse interpolation between each frame matrix represent the "correct" path - reducing the overall rotation and reducing the rotation between each frame adding more frames may alleviate those problems - but I don't really know how ca internals on the iphone works. – Lorenzo Boccaccia Mar 21 '10 at 23:26
  • Sorry i accidently undoed my vote and now im stuck with http://meta.stackexchange.com/questions/21462/undoing-an-old-vote-cannot-be-recast-because-vote-is-too-old – Mattias Wadman Apr 06 '10 at 16:25
  • There is no need to fake anything. See the solution with the gif, below. – bunkerdive May 02 '17 at 16:37
  • are you so desperate for fake internet point to come and advertise your solution for a question that has been dead for SEVEN years? Moreover a solution that requires the installation of a WHOLE NEW STACK and changing the whole project language? – Lorenzo Boccaccia May 06 '17 at 19:09
4

I got it solved. You probably already have a solution as well, but here is what I have found. It is quite simple really...

You can use a CABasicAnimation to do the diagonal rotation, but it needs to be the concatenation of two matrices, namely the existing matrix of the layer, plus a CATransform3DRotate. The "trick" is, in the 3DRotate you need to specify the coordinates to rotate around.

The code looks something like this:


CATransform3DConcat(theLayer.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0));

This will make a rotation that appears as though the upper-left corner of the square is rotating around the axis Y=X, and travelling to the lower-right corner.

The code to animate looks like this:


CABasicAnimation *ani1 = [CABasicAnimation animationWithKeyPath:@"transform"];

// set self as the delegate so you can implement (void)animationDidStop:finished: to handle anything you might want to do upon completion
[ani1 setDelegate:self];

// set the duration of the animation - a float
[ani1 setDuration:dur];

// set the animation's "toValue" which MUST be wrapped in an NSValue instance (except special cases such as colors)
ani1.toValue = [NSValue valueWithCATransform3D:CATransform3DConcat(theLayer.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0))];

// give the animation a name so you can check it in the animationDidStop:finished: method
[ani1 setValue:@"shrink" forKey:@"name"];

// finally, apply the animation
[theLayer addAnimation:ani1 forKey@"arbitraryKey"];

That's it! That code will rotate the square (theLayer) to invisibility as it travels 90-degrees and presents itself orthogonally to the screen. You can then change the color, and do the exact same animation to bring it back around. The same animation works because we are concatenating the matrices, so each time you want to rotate, just do this twice, or change M_PI/2 to M_PI.

Lastly, it should be noted, and this drove me nuts, that upon completion, the layer will snap back to its original state unless you explicitly set it to the end-animation state. This means, just before the line [theLayer addAnimation:ani1 forKey@"arbitraryKey"]; you will want to add


theLayer.transform = CATransform3DConcat(v.theSquare.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0));

to set its value for after the animation completes. This will prevent the snapping back to original state.

Hope this helps. If not you then perhaps someone else who was banging their head against the wall like we were! :)

Cheers,

Chris

Raconteur
  • 1,381
  • 1
  • 15
  • 29
  • I think i tried a similar solution but with the problem that the rotate transform rotates the actual image content. But if the layer is a symmetric image you will not notice it. – Mattias Wadman Jun 25 '10 at 20:59
  • can you explain what `v.theSquare` is here? – aBikis Jun 08 '17 at 18:58
1

Here is a Xamarin iOS example I use to flap the corner of a square button, like a dog ear (easily ported to obj-c):

enter image description here

Method 1: use a rotation animation with 1 for both x and y axes (examples in Xamarin.iOS, but easily portable to obj-c):

// add to the UIView subclass you wish to rotate, where necessary
AnimateNotify(0.10, 0, UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.BeginFromCurrentState, () =>
{
    // note the final 3 params indicate "rotate around x&y axes, but not z"
    var transf = CATransform3D.MakeRotation(-1 * (nfloat)Math.PI / 4, 1, 1, 0);
    transf.m34 = 1.0f / -500;
    Layer.Transform = transf;
}, null);

Method 2: just add an x-axis rotation, and y-axis rotation to a CAAnimationGroup so they run at the same time:

// add to the UIView subclass you wish to rotate, where necessary
AnimateNotify(1.0, 0, UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.BeginFromCurrentState, () =>
{
    nfloat angleTo = -1 * (nfloat)Math.PI / 4;
    nfloat angleFrom = 0.0f ;
    string animKey = "rotate";

    // y-axis rotation
    var anim = new CABasicAnimation();
    anim.KeyPath = "transform.rotation.y";
    anim.AutoReverses = false;
    anim.Duration = 0.1f;
    anim.From = new NSNumber(angleFrom);
    anim.To = new NSNumber(angleTo);

    // x-axis rotation
    var animX = new CABasicAnimation();
    animX.KeyPath = "transform.rotation.x";
    animX.AutoReverses = false;
    animX.Duration = 0.1f;
    animX.From = new NSNumber(angleFrom);
    animX.To = new NSNumber(angleTo);

    // add both rotations to a group, to run simultaneously
    var animGroup = new CAAnimationGroup();
    animGroup.Duration = 0.1f;
    animGroup.AutoReverses = false;
    animGroup.Animations = new CAAnimation[] {anim, animX};
    Layer.AddAnimation(animGroup, animKey);

    // add perspective
    var transf = CATransform3D.Identity;
    transf.m34 = 1.0f / 500;
    Layer.Transform = transf;

}, null);
bunkerdive
  • 2,031
  • 1
  • 25
  • 28
  • Not sure why this beckons a down-vote. This is a CALayer rotation about a diagonal line. "Show your work", downvoter. – bunkerdive May 23 '17 at 18:19
  • This really nice! exactly what I am looking. I don't see where you have assign corner area, In your example corner is big that is moving like dog ear I want small ear – nikhilgohil11 Jan 29 '18 at 13:03
  • @nikhilgohil11, the big "+" area is a separate button (UIView). The corner button is actually a square (half covered up by large button) which is being rotated. In summary, derive 2 Button sub-classes: BigButton and CornerButton. Put a CornerButton instance beneath a BigButton (half covered), and when CornerButton gets down-pressed, call the rotation code. – bunkerdive Feb 07 '18 at 03:24
  • Thanks @bunkerdive, I will try this in swift – nikhilgohil11 Feb 07 '18 at 12:45