Failed Attempt to use Perspective Transformations in Core Animation - iphone

I'm trying to create a simple iPhone app that displays a picture with a reflection beneath, and I want to rotate the picture around the X axis, using Core Animation.
I've started by creating a new iPhone app using the "View-based Application" template. I added a "Picture.jpg" image file, and then I added this to the view controller:
- (void)awakeFromNib {
CGFloat zDistance = 1500.0f;
// Create perspective transformation
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -zDistance;
// Create perspective transform for reflected layer
CATransform3D reflectedTransform = CATransform3DMakeRotation(M_PI, 1.0f, 0.0f, 0.0f);
reflectedTransform.m34 = 1.0f / -zDistance;
// Create spinning picture
CALayer *spinningLayer = [CALayer layer];
spinningLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
spinningLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
spinningLayer.transform = transform;
// Create reflection of spinning picture
CALayer *reflectionLayer = [CALayer layer];
reflectionLayer.frame = CGRectMake(60.0f, 360.0f, 200.0f, 300.0f);
reflectionLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
reflectionLayer.opacity = 0.4f;
reflectionLayer.transform = reflectedTransform;
// Add layers to the root layer
[self.view.layer addSublayer:spinningLayer];
[self.view.layer insertSublayer:reflectionLayer below:spinningLayer];
// Spin the layers
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
anim.duration = 3.0f;
anim.repeatCount = 1e100f;
[spinningLayer addAnimation:anim forKey:#"anim"];
[reflectionLayer addAnimation:anim forKey:#"anim"];
}
This almost works. The problem is that the spinning of the main picture and of the reflection are not perfectly synchronized. It's very close to perfect, but the edges of the bottom of the picture and of top of the reflection are a few pixels apart. They appear to differ by a few degrees.
On the left side of the screen, the reflection seems to be "ahead" of the picture, and on the right side, the reflection is behind the picture. In other words, it looks like the bottom corners of the top image are pushed toward the screen, while the top corners of the reflection are pulled toward the viewer. This is true both when looking at the front and at the back of the images as they spin.
If I increase zDistance, the effect is less noticeable. If I eliminate the perspective transformations altogether by leaving transform.m34 equal to zero for both layers, then the picture and reflection look to be perfectly synchronized. So I don't think the problem is related to time-synchronization issues.
I suspect my perspective transformations are missing something, but I'm not sure how to determine what's wrong. I think I'm basically doing the same transformation described in this related Stack Overflow question.
Can someone help?

I think I may have found an answer to my question. In my original code, I created the reflection by placing it underneath the picture, and applying the flip and perspective. The right way to do it seems to be to place the reflection at the same position as the picture, apply translation and rotation transforms to get it to the right place, and then apply the perspective transformation.
When animating the spin, it is necessary to spin the reflection in the opposite direction of the picture's animation.
So, here's code that works:
- (void)awakeFromNib {
CGFloat zDistance = 1500.0f;
// Create perspective transformation
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -zDistance;
// Create perspective transform for reflected layer
CATransform3D reflectedTransform = CATransform3DMakeTranslation(0.0f, 300.0f, 0.0f);
reflectedTransform = CATransform3DRotate(reflectedTransform, M_PI, 1.0f, 0.0f, 0.0f);
reflectedTransform.m34 = 1.0f / -zDistance;
// Create spinning picture
CALayer *spinningLayer = [CALayer layer];
spinningLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
spinningLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
spinningLayer.transform = transform;
// Create reflection of spinning picture
CALayer *reflectionLayer = [CALayer layer];
reflectionLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
reflectionLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
reflectionLayer.opacity = 0.4f;
reflectionLayer.transform = reflectedTransform;
// Add layers to the root layer
[self.view.layer addSublayer:spinningLayer];
[self.view.layer insertSublayer:reflectionLayer below:spinningLayer];
// Spin the layers
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
anim.duration = 3.0f;
anim.repeatCount = 1e100f;
[spinningLayer addAnimation:anim forKey:#"anim"];
CABasicAnimation *reflectAnim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
reflectAnim.fromValue = [NSNumber numberWithFloat:0.0f];
reflectAnim.toValue = [NSNumber numberWithFloat:(-2 * M_PI)];
reflectAnim.duration = 3.0f;
reflectAnim.repeatCount = 1e100f;
[reflectionLayer addAnimation:reflectAnim forKey:#"reflectAnim"];
}
I'm not sure exactly why this works. It makes intuitive sense to me that placing the reflection at the same position as the original and then transforming it somehow puts the two images into the same "transformation space", but I'd appreciate it if someone could explain the mathematics behind this.
The full project is available at GitHub: https://github.com/kristopherjohnson/perspectivetest

You can use a CAAnimationGroup object to run two animations on the same time space. That should take care of any synchronization issues.
EDIT: removed silly code.

Related

How to move and rotate object in iphone

I am developing a small game in iphone... Game concept is place an object in top of the bar...Rotating a bar using accelerometer. When bar rotating , i need to move an object with respect to bar. How to implement this concept...Any examples or references?
Rotating both img:
barImg,objImg //UIImageView
barImg.transform = CGAffineTransformMakeRotation(Ypos);
objImg.transform=CGAffineTransformMakeRotation(Ypos);
So for rotating 360 Degree with animation:
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat:-M_PI * 2.0]; // full rotation*/ * rotations * duration ];
rotationAnimation.duration = 1;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = MAXFLOAT;
[rotatingTelIamgeview.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
you can play with toValue for changing the angle.
For moving an object:
CGPoint startPoint = CGPointMake(lvMessageInputBar.animationBubbleImageView.layer.frame.origin.x+lvMessageInputBar.animationBubbleImageView.layer.frame.size.width/2
, lvMessageInputBar.animationBubbleImageView.layer.frame.origin.y +lvMessageInputBar.animationBubbleImageView.layer.frame.size.height/2 );
CGPoint endPoint = CGPointMake(endPoint.origin.x+Endpoint.size.width/2, bubleRect.origin.y+bubleRect.size.height/2);
CGPoint middlePoint = // any point between start and end corrdinates
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, startPoint.x, startPoint.y);
CGPathAddQuadCurveToPoint(path, NULL,middlePoint.x, middlePoint.y,endPoint.x,endPoint.y);
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.delegate = self;
pathAnimation.removedOnCompletion = NO;
pathAnimation.path = path;
[pathAnimation setCalculationMode:kCAAnimationCubic];
[pathAnimation setFillMode:kCAFillModeForwards];
pathAnimation.duration = 0.3;
[lvMessageInputBar.animationBubbleImageView.layer addAnimation:pathAnimation forKey:nil];
The above example moves the layer of the object over a path.but CGPathAddQuadCurveToPoint makes the path not diagonal rather circular path (curve).
you can use CGPathAddPath if you do not want this effect.
If you are new to iPhone I would start with layer tutorial to get the idea behind CALayer and then take a look CALayer animations
CALayers introduction:
https://www.raywenderlich.com/3096-calayers-tutorial-for-ios-introduction-to-calayers
CALayer Animation documentation of from Apple:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html
You can also watch Session 424 (Core Animation in Practice, Part 1) and 425 (Core Animation in Practice, Part 2) of WWDC 2010:
https://developer.apple.com/videos/archive/

Working with CAReplicatorLayer

I've been trying to create a cool text effect with the CAReplicatorlayer, CATextLayer and very basic animations. I'm trying to make the letters look like they are being droped from the top of the screen followed by cool replicators which will become less and less visible. I've managed to do this effect but not completely.
So far this is what I've got:
CATextLayer *messageLayer = [CATextLayer layer];
[messageLayer setForegroundColor:[[UIColor blackColor] CGColor]];
[messageLayer setContentsScale:[[UIScreen mainScreen] scale]];
[messageLayer setFrame:CGRectMake(0, 0, 40, 40)];
[messageLayer setString:#"A"];
CAReplicatorLayer *replicatorX = [CAReplicatorLayer layer];
//Set the replicator's attributes
replicatorX.frame = CGRectMake(0, 0, 40, 40);
replicatorX.anchorPoint = CGPointMake(0,0);
replicatorX.position = CGPointMake(0, 0);
replicatorX.instanceCount = 9;
replicatorX.instanceDelay = 0.15;
replicatorX.instanceAlphaOffset = -0.1;
replicatorX.zPosition = 200;
replicatorX.anchorPointZ = -160;
[replicatorX addSublayer:messageLayer];
[self.view.layer addSublayer:replicatorX];
messageLayer.position = CGPointMake(40, 400);
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
animation.fromValue = [NSNumber numberWithInt:0];;
animation.toValue = [NSNumber numberWithInt:400];
animation.duration = 3;
[messageLayer addAnimation:animation forKey:#"s"];
I have two problems:
The replicated layers are starting in the End point.
When the main layer reach his last animated point the animation stops and the replicated layers can't finish their animation.
Setting the correct values to the animation object's fillMode and removedOnCompletion properties will hopefully solve your problems:
"The replicated layers are starting in the End point."
This is caused by the value assigned to instanceDelay property, which delays the animation with 0.15s in your example. To present the animation from the fromValue, you need to account for this delay by setting the animation's fillMode to kCAFillModeBackwards .
animation.fillMode = kCAFillModeBackwards;
"When the main layer reach his last animated point the animation stops
and the replicated layers could not finish their animation."
This happens because by default "the animation is removed from the target layer's animations upon completion."1. You can override this default behavior by setting the animation attribute removedOnCompletion to NO.
animation.removedOnCompletion = NO;
Hope this helps. :)

UIImageView XY Coordinates with Layer Movement

I move my UIImageView with layer around the screen. When at some point I'm trying to retrieve x and y coordinates of a moved imageView using imageView.frame.origin.x I receive original coordinates not the new ones after the move. How to get X and Y correctly?
Here is my code:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:rect];
imageView.animationImages = [NSArray arrayWithArray:imgNameArr];
imageView.animationDuration = 2;
imageView.animationRepeatCount = 0;
[imageView startAnimating];
[self.view addSubview:imageView];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = speed;
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
CGMutablePathRef pointPath = CGPathCreateMutable();
CGPathMoveToPoint(pointPath, NULL, rect.origin.x, rect.origin.y);
CGPathAddLineToPoint(pointPath, NULL, x, y);
pathAnimation.path = pointPath;
CGPathRelease(pointPath);
[imageView.layer addAnimation:pathAnimation forKey:[NSString stringWithFormat:#"pathAnimation%#",objId]];
[imageView release];
Per the UIView documentation for the frame property:
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
The frame property also becomes undefined if you adjust that view's layer's transform or affineTransform properties (which are actually what the UIView alters when you set its transform). In Cocoa Touch transforms are applied at the time of compositing by the compositor. At present what you seem to get when a transform is applied is the original frame as though there were no transform, but as the docs say it's undefined so don't rely on that.
If you're just moving the view around with no other transformation, I recommend you use the view's center property rather than applying a transform. That will update your frame correctly. Based on empirical evidence, UIKit also seems smart enough that just adjusting the centre doesn't have any performance downside compared to applying a transform.

Perspective and Flipping an UIImageView At The Same Time

I am trying to flip and give perspective rotate an uiimageview, in order to obtain an effect like reflection. first i give perspective using CATransform3D and then flip using CGAffineTransformMake. However i loose the perspective effect after the second trasnformation. and i couldn't figure out how to perspective and flip both using CATransform3D. img is the first image and img2 will be its reflection.
CALayer *layer = img.layer;
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -600;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 30.0f * M_PI / 180.0f, 0.0f, 1.0f, 0.0f);
layer.transform = rotationAndPerspectiveTransform;
img2.transform = CGAffineTransformMake(
1, 0, 0, -1, 0, img2.bounds.size.height
);
I was able to accomplish this effect in 4 (optionally 5) steps:
Create two UIImageViews in your nib. Create a UIImageView for your main image, then another for your reflected image. Make sure their view content modes are both set to Aspect Fit. Also, make the frame size of the reflected image 2x that of your main image (because the perspective reflection will add width to the image).
Load up the main image as normal. Now load up the reflection to be upside-down like so:
reflectionView.image = [UIImage
imageWithCGImage:mainImageView.image.CGImage
scale:1.0f
orientation: UIImageOrientationDownMirrored];
Now create the perspective transform like so:
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m24 = 1.0f/-mainImageView.frame.size.width;
[[reflectionView layer] setTransform:rotationAndPerspectiveTransform];
Finally, move the reflection into place:
reflectionView.center = CGPointMake(self.view.frame.size.width/2, mainImageView.frame.size.height*1.5);
Optionally, add a gradient to make the reflection fade away into the
background:
reflectionView.layer.masksToBounds = YES;
// define gradient
CAGradientLayer *theGradient = [CAGradientLayer layer];
theGradient.frame = reflectionView;
theGradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor clearColor] CGColor],
(id)[[UIColor blackColor] CGColor], nil];
// add gradient to reflection image
[reflectionView insertSublayer:theGradient atIndex:0];
reflectionView = 0.25f;
It looks like this:

Why CABasicAnimation will send the layer's view to back and front?

There are two UIViews of similar size. UIView one (A) is originally on top of UIView two (B). When I try to perform a CABasicAnimation transform.rotation.y on A's layer:
CABasicAnimation *rotateAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
CGFloat startValue = 0.0;
CGFloat endValue = M_PI;
rotateAnimation.fromValue = [NSNumber numberWithDouble:startValue];
rotateAnimation.toValue = [NSNumber numberWithDouble:endValue];
rotateAnimation.duration = 5.0;
[CATransaction begin];
[imageA.layer addAnimation:rotateAnimation forKey:#"rotate"];
[CATransaction commit];
During the animation, the animating layer's UIView (A) will be:
sent back (A is suddenly behind B)
rotating ... passed second half of the animation
sent front (A is now on top of B again)
Is there a way to keep A on top of B for the whole animation period? Thanks!
UPDATE: project source is attached: FlipLayer.zip
The problem is a rotation around the Y axis will, in a true 3D compositing system, make layer A intersect with layer B, such that half of layer A should be visible and half should be behind layer B. CoreAnimation does not provide a true 3D compositing system. Instead it attempts to determine which layer is in front of which other layer, and renders it entirely without intersection. In your case, during the first half of the rotation CoreAnimation has decided that layer B is on top of layer A in the 3D space. The simplest solution is probably to set the zPosition of layer A to layer.bounds.size.width (default is 0) for the duration of the animation (or permanently, if it should always be on top).
If you want to set it only for the duration of the animation, you can add a second animation for the transform.translation.z key path. Here's your -flip method modified to use this:
- (IBAction)flip:(id)sender {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
CGFloat startValue = 0.0;
CGFloat endValue = M_PI;
animation.fromValue = [NSNumber numberWithDouble:startValue];
animation.toValue = [NSNumber numberWithDouble:endValue];
animation.duration = 5.0;
[imageOne.layer addAnimation:animation forKey:#"rotate"];
animation.keyPath = #"transform.translation.z";
animation.fromValue = [NSNumber numberWithFloat:imageOne.layer.bounds.size.width];
animation.toValue = animation.fromValue;
[imageOne.layer addAnimation:animation forKey:#"zPosition"];
}
Have you tried adding a [self.view bringSubviewToFront:imageA]; in your animation sequence?
You could try to translate on the z axis and bring A forward first. Perform the rotation and then set the z back to its starting value.
Also, you probably won't see any perspective on the rotation unless you set the .m34 property on the transform. Try something like this:
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -300.0f;
transform = CATransform3DRotate(transform, 0.0f, M_PI, 0.0f);
CABasicAnimation *rotateAnimation = [CABasicAnimation
animationWithKeyPath:#"transform"];
rotateAnimation.fromValue = [NSValue
valueWithCATransform3D(CATransformIdentity)];
rotateAnimation.toValue = [NSValue valueWithCATransform3D(transform)];
rotateAnimation.duration = 5.0;
[imageA.layer addAnimation:rotateAnimation forKey:#"rotate"];
You transaction block is unnecessary.
Best Regards.