iPhone CALayer animation - iphone

I am drawing a line animation, connecting 2 points with a line. This is my code:
-(void) drawObject{
rootLayer = [CALayer layer];
rootLayer.frame = self.view.bounds;
[self.view.layer addSublayer:rootLayer];
lineStartPath = CGPathCreateMutable();
CGPathMoveToPoint(lineStartPath, nil, 160, 50);
CGPathAddLineToPoint(lineStartPath, nil, 160, 50);
CGPathCloseSubpath(lineStartPath);
lineEndPath = CGPathCreateMutable();
CGPathMoveToPoint(lineEndPath, nil, 160, 50);
CGPathAddLineToPoint(lineEndPath, nil, 160, 300);
CGPathCloseSubpath(lineEndPath);
shapeLayer = [CAShapeLayer layer];
shapeLayer.path = lineStartPath;
UIColor *strokeColor = [UIColor colorWithHue:0.557 saturation:0.55 brightness:0.96 alpha:1.0];
shapeLayer.strokeColor = strokeColor.CGColor;
shapeLayer.lineWidth = 3.0;
[rootLayer addSublayer:shapeLayer];
[self performSelector:#selector(startAnimation) withObject:nil afterDelay:1.0];
}
-(void) startAnimation{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 0.5;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.fromValue = (id)lineStartPath;
animation.toValue = (id)lineEndPath;
[shapeLayer addAnimation:animation forKey:#"animatePath"];
}
The weak point is after the animation is ended, the line disappears. I want that after my line is drawn between those 2 points, id does not disappear. Give me a hint please.

Try adding
shapeLayer.path = lineEndPath;
at the beginning of startAnimation.
It's explained in this video: Session 424 - Core Animation in Practice, Part 1 from the WWDC 2010 (around 38:00).
and replace [shapeLayer addAnimation:animation forKey:#"animatePath"]; with [shapeLayer addAnimation:animation forKey:#"path"];.

Related

CAShapeLayer LineCap not proper at end of CABasic Animation

I have implemented a basic line drawing animation for a progress bar.The line has a round cap edge.When I animated the line drawing the beginning of line is having a rounded cap but in the end its a square. below is the code I am using am i missing anything??
CAShape Layer Code :
CAShapeLayer *lineLayer = [[CAShapeLayer alloc] init];
[lineLayer setFrame: CGRectMake(0,
0,
wifisyncView.popUpView.frame.size.width,
wifisyncView.popUpView.frame.size.height)];
[lineLayer setPath: [linePath CGPath]];
lineLayer.lineWidth = 9.0f;
lineLayer.strokeEnd = 9.0f;
lineLayer.fillColor = [[UIColor orangeColor] CGColor];
lineLayer.lineCap = kCALineCapRound;
[lineLayer setStrokeColor:[[UIColor orangeColor] CGColor]];
[lineLayer setMasksToBounds:YES];
lineLayer.cornerRadius = 30.0f;
CABASIC Animation code :
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 5 seconds "
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = NO;// Remain stroked after the animation..
drawAnimation.fillMode = kCAFillModeForwards;
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
drawAnimation.delegate = self;
// Add the animation to the circle
[lineLayer addAnimation:drawAnimation forKey:#"drawLineAnimation"];
Image Attached :
I dont find any mistakes in your code. I created a new project and pasted your code to viewDidLoad and is working fine. Code for your reference.
UIBezierPath *linePath = [UIBezierPath bezierPath];
[linePath moveToPoint:CGPointMake(100, 100)];
[linePath addLineToPoint:CGPointMake(500, 100)];
CAShapeLayer *lineLayer = [[CAShapeLayer alloc] init];
[lineLayer setFrame:self.view.bounds];
[lineLayer setPath: [linePath CGPath]];
lineLayer.lineWidth = 9.0f;
lineLayer.strokeEnd = 9.0f;
lineLayer.fillColor = [[UIColor orangeColor] CGColor];
lineLayer.lineCap = kCALineCapRound;
[lineLayer setStrokeColor:[[UIColor orangeColor] CGColor]];
[lineLayer setMasksToBounds:YES];
lineLayer.cornerRadius = 30.0f;
[self.view.layer addSublayer:lineLayer];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 5 seconds "
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = NO;// Remain stroked after the animation..
drawAnimation.fillMode = kCAFillModeForwards;
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
drawAnimation.delegate = self;
// Add the animation to the circle
[lineLayer addAnimation:drawAnimation forKey:#"drawLineAnimation"];
I think you should try changing
[lineLayer setFrame: CGRectMake(0,
0,
wifisyncView.popUpView.frame.size.width,
wifisyncView.popUpView.frame.size.height)];
to
[lineLayer setFrame: CGRectMake(0,
0,
wifisyncView.popUpView.bounds.size.width,
wifisyncView.popUpView.bounds.size.height)];

CAShapeLayer and animation position issue

I am making an iPhone app that draws a circle on the screen. I use a timer (every 5 second), so that I can 'grow' the circle, then wait 5 seconds then grow again. Now I am having trouble when animating the increase in size of the circle (I use paths and a CAShapeLayer layer). The size (from and to) are fine with the animation, its just when it starts the circle moves to the upper left hand side and grows from there. Any help or suggestions would be great. Thanks
The class where this is implemented is a UIControl, which is added to a UIView.
//Called in init method
(void) initalPop {
//Circle
self.bubble = [CAShapeLayer layer];
self.bubble.bounds = self.bounds;
self.bubble.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
self.bubble.fillColor = [
[UIColor greenColor] CGColor
];
self.bubble.strokeColor = [
[UIColor greenColor] CGColor
];
self.bubble.lineWidth = 4.0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, nil, self.bounds);
self.bubble.path = path;
[self.layer addSublayer: self.bubble];
}
//Called when timer goes off
- (void) StageGrow {
CGFloat growSize = 50.0;
//Change the size of us and center the circle (but dont want to animate this
[CATransaction begin];
[CATransaction setValue: (id) kCFBooleanTrue forKey: kCATransactionDisableActions];
self.bounds = CGRectMake(0, 0, self.bounds.size.width + growSize, self.bounds.size.height + growSize);
self.bubble.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[CATransaction commit];
[self ActualGrowCircle];
} - (void) ActualGrowCircle {
CGMutablePathRef oldPath = CGPathCreateMutable();
CGPathAddEllipseInRect(oldPath, nil, self.bubble.bounds);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, nil, self.bounds);
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath: #"path"];
animation.duration = 2.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];
animation.repeatCount = 1;
animation.delegate = self;
animation.autoreverses = NO;
animation.fromValue = (__bridge id) oldPath;
animation.toValue = (__bridge id) path;
self.bubble.bounds = self.bounds;
self.bubble.path = path;
[self.bubble addAnimation: animation forKey: #"animatePath"];
}
Let's say you're growing the bubble from size (100, 100) to size (150, 150).
You create oldPath as an ellipse in the rectangle (0, 0, 100, 100).
You create path as an ellipse in the rectangle (0, 0, 150, 150).
You set the bounds of the bubble layer to (0, 0, 150, 150), and you don't animate this change.
That means that oldPath will appear aligned to the top and left edges of the bubble layer.
One way to fix this is to create oldPath in a rectangle that is centered in the bubble's new bounds:
CGRect oldBubbleRect = CGRectInset(self.bounds, growSize / 2, growSize / 2);
CGPathRef oldPath = CGPathCreateWithEllipseInRect(oldBubbleRect, NULL);
By the way, you are leaking the paths you're creating. You need to call CGPathRelease on them after you're done with them.
Worked it out. In the long run I grouped the animations together and all became good (see code below). Thanks for your help Rob.
-(void)ActualGrowCircle {
CGMutablePathRef newPath = CGPathCreateMutable();
CGPathAddEllipseInRect(newPath, nil, self.bounds);
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.repeatCount = 1;
animation.autoreverses = NO;
animation.fromValue = (__bridge id)self.bubble.path;
animation.toValue = (__bridge id)newPath;
CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:#"bounds"];
animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation2.repeatCount = 1;
animation2.autoreverses = NO;
animation2.fromValue = [NSValue valueWithCGRect:self.bubble.bounds];
animation2.toValue = [NSValue valueWithCGRect:self.bounds];
CAAnimationGroup *group = [CAAnimationGroup animation];
[group setAnimations:[NSArray arrayWithObjects: animation2, animation, nil]];
group.duration = 0.5;
group.delegate = self;
self.bubble.bounds = self.bounds;
self.bubble.path = newPath;
[self.bubble addAnimation:group forKey:#"animateGroup"];
CGPathRelease(newPath);
}

How to draw line with animation in iPhone

I am drawing line in iphone using CGContextRef. Can any one suggest me how i draw line with animation in iphone.
Please suggest.
Here is the answer (found here : http://soulwithmobiletechnology.blogspot.fr/2012/07/how-to-animate-line-draw.html)
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(50.0,0.0)];
[path addLineToPoint:CGPointMake(120.0, 600.0)];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.view.bounds;
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [[UIColor redColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 2.0f;
pathLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:pathLayer];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 2.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
Do not forget to #import <QuartzCore/QuartzCore.h>.
Is it a straight line?.. You can use a 1 pixel wide UIView and animate its frame property. Using [UIView beginAnimations] and [UIView commitAnimations]; Otherwise, see this post.
Edited response
Using core animation, you can do something like this (e.g. behind a button)
CABasicAnimation *theAnimation = [self pathAnimation];
theAnimation.duration=3.0;
theAnimation.autoreverses=YES;
[[self layer] addAnimation:theAnimation forKey:#"animatePath"];
Path animation is defined as:
- (CAAnimation*)pathAnimation;
{
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path,NULL,50.0,120.0);
CGPathAddCurveToPoint(path,NULL,50.0,275.0,
150.0,275.0,
150.0,120.0);
CGPathAddCurveToPoint(path,NULL,150.0,275.0,
250.0,275.0,
250.0,120.0);
CGPathAddCurveToPoint(path,NULL,250.0,275.0,
350.0,275.0,
350.0,120.0);
CGPathAddCurveToPoint(path,NULL,350.0,275.0,
450.0,275.0,
450.0,120.0);
CAKeyframeAnimation *
animation = [CAKeyframeAnimation
animationWithKeyPath:#"position"];
[animation setPath:path];
[animation setDuration:3.0];
[animation setAutoreverses:YES];
CFRelease(path);
return animation;
}
This is only a pointer to what you can do with Core Animation. See this article for detail.
The Swift 3 version of Martin's answer (checked with Xcode 8.3.3)
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 0))
path.addLine(to: CGPoint(x: 120, y: 600))
let pathLayer = CAShapeLayer()
pathLayer.frame = view.bounds
pathLayer.path = path.cgPath
pathLayer.strokeColor = UIColor.red.cgColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 2
pathLayer.lineJoin = kCALineJoinBevel
view.layer.addSublayer(pathLayer)
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 2.0
pathAnimation.fromValue = 0
pathAnimation.toValue = 1
pathLayer.add(pathAnimation, forKey: "strokeEnd")

How to adjust drop shadow dynamically during an UIImageView rotation?

I use the following to code to add drop shadow:
letterE.layer.shadowColor = [[UIColor blackColor] CGColor];
letterE.layer.shadowOffset = CGSizeMake(2.5, 2.5);
letterE.layer.shadowRadius = 3.0;
letterE.layer.shadowOpacity = 0.95;
and the following to rotate:
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
rotationAnimation.duration = 5.0;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = 1.0;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[letterE.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
During the animation, the shadow is static which looks weird:
How can I make the shadow dynamically updated during the animation?
I found this interesting so I gave it a shot. A possible solution is to build a second clear view under the main view, giving it (the bottom view) a shadow using the original view's path. Then you can apply the animation to both views. I did this with simple rectangular views but I see no reason why this can't be done with more complex paths or using 2 CALayers instead of 2 UIViews.
This is how I set up my views...
testView = [[UIView alloc] initWithFrame:CGRectMake(40, 40, 100, 100)];
testView.backgroundColor = [UIColor redColor];
//increase y origin of second view to simulate shadow offset
testViewShadow = [[UIView alloc] initWithFrame:CGRectMake(40, 50, 100, 100)];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, testView.frame.size.width, testView.frame.size.height));
testViewShadow.layer.shadowPath = path;
testViewShadow.layer.shadowOpacity = 1.0;
testViewShadow.layer.shadowOffset = CGSizeMake(0, 0);
testViewShadow.layer.shadowRadius = 10.0;
CFRelease(path);
[self.view addSubview:testViewShadow];
[self.view addSubview:testView];
..and my animation (just hooked up your CAAnimation to a button as an IBAction and applied to both views)...
- (IBAction)rotate:(id)sender{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
rotationAnimation.duration = 5.0;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = 1.0;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[testView.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
[testViewShadow.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
This is what the result looks like...
Any (2D) transformations should look believable if you apply them to both views.
Hope that helps!

Origami transition using CATransform3D perspective

I'm trying to achieve a kind of origami transition on two UIView using only layer capabilities. The idea is to fold two views with a perspective effect. Both views have a perspective, the transition is defined by a rotation on each view, as well as a translation on one view such that this view seems to be attached to the other one.
The issue is that the view overlaps one another in the middle of the transition. I don't want to use a zPosition to visually avoid this overlapping, I really want these two views to act as if they were bound together by their shared side. Here is the code for the transition.
Any idea, or any other solution?
- (void)animateWithPerspective
{
CGFloat rotationAngle = 90;
CATransform3D transform = CATransform3DIdentity;
UIView *topView;
UIView *bottomView;
UIView *mainView;
CGRect frame;
CGFloat size = 200;
mainView = [[UIView alloc] initWithFrame:CGRectMake(10,10, size, size*2)];
[self.view addSubview:mainView];
bottomView = [[UIView alloc] initWithFrame:CGRectZero];
bottomView.layer.anchorPoint = CGPointMake(0.5, 1);
bottomView.frame = CGRectMake(0, size, size, size);
bottomView.backgroundColor = [UIColor blueColor];
[mainView addSubview:bottomView];
topView = [[UIView alloc] initWithFrame:CGRectZero];
topView.layer.anchorPoint = CGPointMake(0.5, 0);
topView.frame = CGRectMake(0, 0, size, size);
topView.backgroundColor = [UIColor redColor];
[mainView addSubview:topView];
transform.m34 = 1.0/700.0;
topView.layer.transform = transform;
bottomView.layer.transform = transform;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:2];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationRepeatCount:INFINITY];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
frame = bottomView.frame;
frame.origin.y = bottomView.frame.origin.y - bottomView.frame.size.height - topView.frame.size.height;
bottomView.frame = frame;
topView.layer.transform = CATransform3DRotate(transform, rotationAngle * M_PI/180, 1, 0, 0);
bottomView.layer.transform = CATransform3DRotate(transform, -rotationAngle * M_PI/180, 1, 0, 0);
[UIView commitAnimations];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self animate];
}
To simplify the problem, let's get rid of any perspective transform. Here is a simpler code with the same kind of issue:
- (void)animateWithoutPerspective
{
CGFloat rotationAngle = 90;
UIView *topView;
UIView *bottomView;
UIView *mainView;
CGRect frame;
CGFloat size = 200;
mainView = [[UIView alloc] initWithFrame:CGRectMake(10,10, size, size*2)];
[self.view addSubview:mainView];
bottomView = [[UIView alloc] initWithFrame:CGRectMake(0, size, size, size)];
bottomView.backgroundColor = [UIColor blueColor];
[mainView addSubview:bottomView];
topView = [[UIView alloc] initWithFrame:CGRectZero];
topView.layer.anchorPoint = CGPointMake(0.5, 0);
topView.frame = CGRectMake(10, 0, size-20, size);
topView.backgroundColor = [UIColor redColor];
[mainView addSubview:topView];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:2];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationRepeatCount:INFINITY];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
frame = bottomView.frame;
frame.origin.y = bottomView.frame.origin.y - bottomView.frame.size.height;
bottomView.frame = frame;
topView.layer.transform = CATransform3DMakeRotation(rotationAngle * M_PI/180, 1, 0, 0);
[UIView commitAnimations];
}
Finally, here is some solution for a three-sleeves animation with simple shadows added. The key to solve this kind of animation is to use several well organized sublayers and also some CATransformLayer.
- (void)animate
{
CATransform3D transform = CATransform3DIdentity;
CALayer *topSleeve;
CALayer *middleSleeve;
CALayer *bottomSleeve;
CALayer *topShadow;
CALayer *middleShadow;
UIView *mainView;
CGFloat width = 300;
CGFloat height = 150;
CALayer *firstJointLayer;
CALayer *secondJointLayer;
CALayer *perspectiveLayer;
mainView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, width, height*3)];
mainView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:mainView];
perspectiveLayer = [CALayer layer];
perspectiveLayer.frame = CGRectMake(0, 0, width, height*2);
[mainView.layer addSublayer:perspectiveLayer];
firstJointLayer = [CATransformLayer layer];
firstJointLayer.frame = mainView.bounds;
[perspectiveLayer addSublayer:firstJointLayer];
topSleeve = [CALayer layer];
topSleeve.frame = CGRectMake(0, 0, width, height);
topSleeve.anchorPoint = CGPointMake(0.5, 0);
topSleeve.backgroundColor = [UIColor redColor].CGColor;
topSleeve.position = CGPointMake(width/2, 0);
[firstJointLayer addSublayer:topSleeve];
topSleeve.masksToBounds = YES;
secondJointLayer = [CATransformLayer layer];
secondJointLayer.frame = mainView.bounds;
secondJointLayer.frame = CGRectMake(0, 0, width, height*2);
secondJointLayer.anchorPoint = CGPointMake(0.5, 0);
secondJointLayer.position = CGPointMake(width/2, height);
[firstJointLayer addSublayer:secondJointLayer];
middleSleeve = [CALayer layer];
middleSleeve.frame = CGRectMake(0, 0, width, height);
middleSleeve.anchorPoint = CGPointMake(0.5, 0);
middleSleeve.backgroundColor = [UIColor blueColor].CGColor;
middleSleeve.position = CGPointMake(width/2, 0);
[secondJointLayer addSublayer:middleSleeve];
middleSleeve.masksToBounds = YES;
bottomSleeve = [CALayer layer];
bottomSleeve.frame = CGRectMake(0, height, width, height);
bottomSleeve.anchorPoint = CGPointMake(0.5, 0);
bottomSleeve.backgroundColor = [UIColor grayColor].CGColor;
bottomSleeve.position = CGPointMake(width/2, height);
[secondJointLayer addSublayer:bottomSleeve];
firstJointLayer.anchorPoint = CGPointMake(0.5, 0);
firstJointLayer.position = CGPointMake(width/2, 0);
topShadow = [CALayer layer];
[topSleeve addSublayer:topShadow];
topShadow.frame = topSleeve.bounds;
topShadow.backgroundColor = [UIColor blackColor].CGColor;
topShadow.opacity = 0;
middleShadow = [CALayer layer];
[middleSleeve addSublayer:middleShadow];
middleShadow.frame = middleSleeve.bounds;
middleShadow.backgroundColor = [UIColor blackColor].CGColor;
middleShadow.opacity = 0;
transform.m34 = -1.0/700.0;
perspectiveLayer.sublayerTransform = transform;
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.x"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:0]];
[animation setToValue:[NSNumber numberWithDouble:-90*M_PI/180]];
[firstJointLayer addAnimation:animation forKey:nil];
animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.x"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:0]];
[animation setToValue:[NSNumber numberWithDouble:180*M_PI/180]];
[secondJointLayer addAnimation:animation forKey:nil];
animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.x"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:0]];
[animation setToValue:[NSNumber numberWithDouble:-90*M_PI/180]];
[bottomSleeve addAnimation:animation forKey:nil];
animation = [CABasicAnimation animationWithKeyPath:#"bounds.size.height"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:perspectiveLayer.bounds.size.height]];
[animation setToValue:[NSNumber numberWithDouble:0]];
[perspectiveLayer addAnimation:animation forKey:nil];
animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:perspectiveLayer.position.y]];
[animation setToValue:[NSNumber numberWithDouble:0]];
[perspectiveLayer addAnimation:animation forKey:nil];
animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:0]];
[animation setToValue:[NSNumber numberWithDouble:0.5]];
[topShadow addAnimation:animation forKey:nil];
animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
[animation setDuration:2];
[animation setAutoreverses:YES];
[animation setRepeatCount:INFINITY];
[animation setFromValue:[NSNumber numberWithDouble:0]];
[animation setToValue:[NSNumber numberWithDouble:0.5]];
[middleShadow addAnimation:animation forKey:nil];
}
Swift version of Phil's answer
func animate() {
var transform:CATransform3D = CATransform3DIdentity;
var topSleeve:CALayer
var middleSleeve:CALayer
var bottomSleeve:CALayer
var topShadow:CALayer
var middleShadow:CALayer
var mainView:UIView
var width:CGFloat = 300
var height:CGFloat = 150
var firstJointLayer:CALayer
var secondJointLayer:CALayer
var perspectiveLayer:CALayer
mainView = UIView(frame:CGRectMake(50, 50, width, height*3))
mainView.backgroundColor = UIColor.yellowColor()
view.addSubview(mainView)
perspectiveLayer = CALayer()
perspectiveLayer.frame = CGRectMake(0, 0, width, height*2)
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer = CATransformLayer()
firstJointLayer.frame = mainView.bounds;
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve = CALayer()
topSleeve.frame = CGRectMake(0, 0, width, height);
topSleeve.anchorPoint = CGPointMake(0.5, 0)
topSleeve.backgroundColor = UIColor.redColor().CGColor;
topSleeve.position = CGPointMake(width/2, 0)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer = CATransformLayer()
secondJointLayer.frame = mainView.bounds;
secondJointLayer.frame = CGRectMake(0, 0, width, height*2)
secondJointLayer.anchorPoint = CGPointMake(0.5, 0)
secondJointLayer.position = CGPointMake(width/2, height)
firstJointLayer.addSublayer(secondJointLayer)
middleSleeve = CALayer()
middleSleeve.frame = CGRectMake(0, 0, width, height);
middleSleeve.anchorPoint = CGPointMake(0.5, 0)
middleSleeve.backgroundColor = UIColor.blueColor().CGColor
middleSleeve.position = CGPointMake(width/2, 0)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve = CALayer()
bottomSleeve.frame = CGRectMake(0, height, width, height)
bottomSleeve.anchorPoint = CGPointMake(0.5, 0)
bottomSleeve.backgroundColor = UIColor.grayColor().CGColor
bottomSleeve.position = CGPointMake(width/2, height)
secondJointLayer.addSublayer(bottomSleeve)
firstJointLayer.anchorPoint = CGPointMake(0.5, 0)
firstJointLayer.position = CGPointMake(width/2, 0)
topShadow = CALayer()
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.blackColor().CGColor
topShadow.opacity = 0
middleShadow = CALayer()
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.blackColor().CGColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform;
var animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = -90*M_PI/180
firstJointLayer.addAnimation(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = 180*M_PI/180
secondJointLayer.addAnimation(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = -90*M_PI/180
bottomSleeve.addAnimation(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.height")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = perspectiveLayer.bounds.size.height
animation.toValue = 0
perspectiveLayer.addAnimation(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.y")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = perspectiveLayer.position.y
animation.toValue = 0
perspectiveLayer.addAnimation(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = 0.5
topShadow.addAnimation(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.addAnimation(animation, forKey: nil)
}
At first I though that the linear transformation of the Y position would not imply a linear transformation of the rotation, but it seems that it is the case.
The error is very simple, the perspective value is wrong, the perspective is modeled by positioning an observatory on the Z axis at a negative distance. then you need to negate the perspective value :
transform.m34 = 1.0/(-700.0);
And it does work like expected.
Just for the record, the transformation is not linear for the angles. but the artifact are hidden by the zbuffer.
At mid path the angle would be 60 degree but with the linear animation we get 45 degree. But looking from the right side, from negative Z axis position, the buffer hide the planes intersection.
To illustrate the answers.
I did not put all the animations and the perspective projection (the perspectiveLayer.sublayerTransform on its CATransformLayer subLayers). Play with the projection matrix m34 field value to see how it affects the vanishing point.
Swift 4 updated answer:
updated version of Vojtec's answer.
func animate() {
var transform:CATransform3D = CATransform3DIdentity;
var topSleeve:CALayer
var middleSleeve:CALayer
var bottomSleeve:CALayer
var topShadow:CALayer
var middleShadow:CALayer
var mainView:UIView
let width:CGFloat = 300
let height:CGFloat = 150
var firstJointLayer:CALayer
var secondJointLayer:CALayer
var perspectiveLayer:CALayer
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width, height: height*3))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer = CALayer()
perspectiveLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer = CATransformLayer()
firstJointLayer.frame = mainView.bounds;
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve = CALayer()
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width/2, y: 0)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer = CATransformLayer()
secondJointLayer.frame = mainView.bounds;
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
secondJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
secondJointLayer.position = CGPoint(x: width/2, y: height)
firstJointLayer.addSublayer(secondJointLayer)
middleSleeve = CALayer()
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height);
middleSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width/2, y: 0)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve = CALayer()
bottomSleeve.frame = CGRect(x: 0, y: height, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width/2, y: height)
secondJointLayer.addSublayer(bottomSleeve)
firstJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
firstJointLayer.position = CGPoint(x: width/2, y: 0)
topShadow = CALayer()
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow = CALayer()
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform;
var animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = -90*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = 180*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = -90*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.height")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = perspectiveLayer.bounds.size.height
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.y")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = perspectiveLayer.position.y
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 2
animation.autoreverses = true
animation.repeatCount = 1000
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
}
}