How to rotate a flat object around its center in perspective view? - ios

What I'm trying to do seems like it should be easy enough: I created a 2D top-down view of an old phonograph record. I want to rotate it (lay it back) in its X axis and then spin it around its Z axis.
I've read every question here that has CATransform3D in the body, I've read Steve Baker's "Matrices can be your friends" article as well as Bill Dudney's book "Core Animation for Mac OS X and the iPhone" I think Brad Larson's "3-D Rotation without a trackball" has all the right code, but since he's permitting the user to adjust all three axis, I'm having a hard time shrinking his code into what I perceive to be just one dimension (a rotated z axis).
Here's the image I'm testing with, not that the particulars are important to the problem:
I bring that onscreen the usual way: (in a subclass of UIView)
- (void)awakeFromNib
{
UIImage *recordImage = [UIImage imageNamed:#"3DgoldRecord"];
if (recordImage) {
recordLayer = [CALayer layer];
[recordLayer setFrame:CGRectMake(0.0, 0.0, 1024, 1024)];
[recordLayer setContents:(id)[[UIImage imageNamed:#"3DgoldRecord"] CGImage]];
[self.layer addSublayer:recordLayer];
}
}
That's the first test, just getting it on the screen ;-)
Then, to "lay it back" I apply a transform to rotate about the layer's X axis, inserting this code after setting the contents of the layer to the image and before adding the sublayer:
CATransform3D myRotationTransform =
CATransform3DRotate(recordLayer.transform,
(M_PI_2 * 0.85), //experiment with flatness
1.0, // rotate only across the x-axis
0.0, // no y-axis transform
0.0); // no z-axis transform
recordLayer.transform = myRotationTransform;
That worked as expected: The record is laying back nicely.
And for the next step, causing the record to spin, I tied this animation to the touchesEnded event, although once out of the testing/learning phase this rotation won't be under user control:
CATransform3D currentTransform = recordLayer.transform; // to come back to at the end
CATransform3D myRotationTransform =
CATransform3DRotate(currentTransform,
1.0, // go all the way around once
(M_PI_2 * 0.85), // x-axis change
1.00, // y-axis change ?
0.0); // z-axis change ?
recordLayer.transform = myRotationTransform;
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
myAnimation.duration = 5.0;
myAnimation.fromValue = [NSNumber numberWithFloat:0.0];
myAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2.0];
myAnimation.delegate = self;
[recordLayer addAnimation:myAnimation forKey:#"transform.rotation"];
So I'm pretty sure what I'm hung up on is the vector in the CATransform3DRotate call (trust me: I've been trying simple changes in that vector to watch the change... what's listed there now is simply the last change I tried). As I understand it, the values for x, y, and z in the transform are, in essence, the percentage of the value passed in during the animation ranging from fromValue to toValue.
If I'm on the right track understanding this, is it possible to express this in a single transform? Or must I, for each effective frame of animation, rotate the original upright image slightly around the z axis and then lay the result down with an x axis rotation? I saw a question/answer that talked about combining transforms, but that was a scale transform followed by a rotation transform. I have messed around with transforming the transform, but isn't doing what I think I should be getting (i.e. passing the result of one transform into the transform argument of the next seemed to just execute one completely and then animate the other).

This is easiest if you use a CATransformLayer as the parent of the image view's layer. So you'll need a custom view subclass that uses CATransformLayer as its layer. This is trivial:
#interface TransformView : UIView
#end
#implementation TransformView
+ (Class)layerClass {
return [CATransformLayer class];
}
#end
Put a TransformView in your nib, and add a UIImageView as a subview of the TransformView. Connect these views to outlets in your view controller called transformView and discView.
In your view controller, set the transform of transformView to apply perspective (by setting m34) and the X-axis tilt:
- (void)viewDidLoad
{
[super viewDidLoad];
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1 / 500.0;
transform = CATransform3DRotate(transform, .85 * M_PI_2, 1, 0, 0);
self.transformView.layer.transform = transform;
}
Add an animation for key path transform.rotation.z to discView in viewWillAppear: and remove it in viewDidDisappear::
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0];
animation.toValue = [NSNumber numberWithFloat:2 * M_PI];
animation.duration = 1.0;
animation.repeatCount = HUGE_VALF;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[self.discView.layer addAnimation:animation forKey:#"transform.rotation.z"];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.discView.layer removeAllAnimations];
}
Result:
UPDATE
Here's a Swift playground demonstration:
import UIKit
import XCPlayground
class TransformView: UIView {
override class func layerClass() -> AnyClass {
return CATransformLayer.self
}
}
let view = UIView(frame: CGRectMake(0, 0, 300, 150))
view.backgroundColor = UIColor.groupTableViewBackgroundColor()
XCPlaygroundPage.currentPage.liveView = view
let transformView = TransformView(frame: view.bounds)
view.addSubview(transformView)
var transform = CATransform3DIdentity
transform.m34 = CGFloat(-1) / transformView.bounds.width
transform = CATransform3DRotate(transform, 0.85 * CGFloat(M_PI_2), 1, 0, 0)
transformView.layer.transform = transform
let image = UIImage(named: "3DgoldRecord")!
let imageView = UIImageView(image: image)
imageView.bounds = CGRectMake(0, 0, 200, 200)
imageView.center = CGPointMake(transformView.bounds.midX, transformView.bounds.midY)
transformView.addSubview(imageView)
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = 2 * M_PI
animation.duration = 1
animation.repeatCount = Float.infinity
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
imageView.layer.addAnimation(animation, forKey: animation.keyPath)
Copy the image from the question into the playground Resources folder and name it 3DgoldRecord.png. Result:

You may consider wrapping the recordLayer with a superlayer. Apply the x-axis rotation to the superlayer and add the z-axis rotation animation to recordLayer.

Related

Making a single image to scroll infinitely over the viewcontroller?

I have two images. first one is something like a transparent hole and should be static while the second one is scooter like image which needs to continuously scrolling horizontally that should give a effect of video.
Can anybody suggest me what to do to achieve the same.
Please check the code Infinity Scroll
You can animate X position of your image view like this:
CGPoint point0 = imView.layer.position;
CGPoint point1 = { NSIntegerMax, point0.y };
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = #(point0.x);
anim.toValue = #(point1.x);
anim.duration = 1.5f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// First we update the model layer's property.
imView.layer.position = point1;
// Now we attach the animation.
[imView.layer addAnimation:anim forKey:#"position.x"];
For infinite scrolling, set your scrollview' contentSize correctly:
scrollView.contentSize = CGSizeMake(NSIntegerMax, kScreenHeight); // Deduce kScreenHeight based on device height

CALayer opacity animation works but bounds animation doesn't

I'm trying to animate the bounds of a CALayer and it's not working. Here's the code:
class CircleView < UIIVew
def initWithFrame(frame)
super
# determine the dimensions
radius = (bounds.size.width < bounds.size.height ? bounds.size.width : bounds.size.height) / 2
# create the circle layer
#circle = CAShapeLayer.layer
#circle.bounds = bounds
#circle.path = UIBezierPath.bezierPathWithRoundedRect(bounds, cornerRadius: radius).CGPath
#circle.fillColor = UIColor.blueColor.CGColor
# add the circle to the view
self.layer.addSublayer(#circle)
shrink
self
end
private
# Applies the shrink animation to the provided circle layer.
def shrink
# determine the bounds
old_bounds = #circle.bounds
new_bounds = CGRectMake(old_bounds.size.width / 2, old_bounds.size.height / 2, 0, 0)
# animate the shrinking
animation = CABasicAnimation.animationWithKeyPath("bounds")
animation.fromValue = NSValue.valueWithCGRect(old_bounds)
animation.toValue = NSValue.valueWithCGRect(new_bounds)
animation.duration = 3.0
#circle.addAnimation(animation, forKey:"bounds")
# set the bounds so the animation doesn't bounce back when it's complete
#circle.bounds = new_bounds
end
# Applies the shrink animation to the provided circle layer.
def disappear
# animate the shirnk
animation = CABasicAnimation.animationWithKeyPath("opacity")
animation.fromValue = NSNumber.numberWithFloat(1.0)
animation.toValue = NSNumber.numberWithFloat(0.0)
animation.duration = 3.0
#circle.addAnimation(animation, forKey:"opacity")
# set the value so the animation doesn't bound back when it's completed
#circle.opacity = 0.0
end
end
If I call disappear from the initWithFrame method, the animation works fine. However, calling shrink does nothing. What am I doing wrong?
That's because you are using a CAShapeLayer. The path is independent of the bounds, so you have to animate it separately.
I could reproduce your problem with a version of your code roughly translated to Objective C. When I changed the shrinkmethod to this, it worked:
-(void)shrink
{
CGRect old_bounds = self.circle.bounds;
CGRect new_bounds = CGRectMake(old_bounds.size.width / 2, old_bounds.size.height / 2, 0, 0);
// bounds
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: #"bounds"];
animation.fromValue = [NSValue valueWithCGRect: old_bounds];
animation.toValue = [NSValue valueWithCGRect: new_bounds];
animation.duration = 3.0;
[self.circle addAnimation: animation forKey: #"bounds"];
// path
CGPathRef old_path = self.circle.path;
CGPathRef new_path = [UIBezierPath bezierPathWithRoundedRect:new_bounds cornerRadius:0].CGPath;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath: #"path"];
pathAnimation.fromValue = (__bridge id)(old_path);
pathAnimation.toValue = (__bridge id)(new_path);
pathAnimation.duration = 3.0;
[self.circle addAnimation: pathAnimation forKey: #"path"];
//set the value so the animation doesn't bound back when it's completed
self.circle.bounds = new_bounds;
self.circle.path = new_path;
}
Technically, you might not even have to animate the bounds. If you set a distinct backgroundColor on your circle layer, you can experiment a little and more easily see how boundsand path can be set and animated more or less independently (for example, in your original code you could see that the bounds are in fact animated but the path stays the same).
Not knowing Ruby, I would suggest adjusting the frame rather than the bounds. The bounds are in the CALayers own coordinate system, and have no bearing on how the layer itself is displayed in the parent.

Rotating a view around a fixed point - ObjectiveC

I need to rotate a UIView around a fixed point.
bubbleTail = [[BubbleTail alloc] initWithFrame:bubbleTailFrame];
[self addSubview:bubbleTail];
bubbleTail.layer.anchorPoint = triangle_top_left;
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI * 180 / 180.0);
bubbleTail.transform = transform;
This works only if I comment the .anchorPoint line.
How do I rotate the view about a point inside the view?
The below should help. The tricky part is that setting the anchorpoint after adding a view with a frame then moves the view. I've gotten around this by initiating the UIView without a frame, then setting the position, bounds and anchorpoint of its layer. I then use the rotation to rotate around the anchor point.
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0];
rotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
rotation.duration = 60.0;
rotation.repeatCount = HUGE_VALF;
UIView *myview = [[UIView alloc] init];
[self.view addSubview:myview];
[myview setBackgroundColor:[UIColor orangeColor]];
[myview.layer setPosition:CGPointMake(10, 10)];
[myview.layer setBounds:CGRectMake(0, 0, 100, 100)];
[myview.layer setAnchorPoint:CGPointMake(0, 0)];
[myview.layer addAnimation:rotation forKey:#"myAnimation"];
Someone please correct me if this is not the best method or if there are mistakes - I only know what I currently know - e.g. when I should use a . or f after a float.
So it seems CGAffineTransformMakeRotation rotates it around the center of the view?
Say you have point P (px,py) in view, you want view to be rotated around that.
Let's A(ax,ay) = P - center = (px - centerx, py - centery).
And you have angle alpha.
Then you could make a translate matrix with - A, then multipy it with rotation matrix with alpha, then multipy it with translate matrix with + A.
The functions you'll need are: CGAffineTransformMakeTranslation, CGAffineTransformMakeRotation, CGAffineTransformConcat.
P.S. Not sure if this will work (also not sure how view rect is represented, maybe it first assumption about rotation around center is wrong, then you shoud assume A = P), but that's how it's done with affine transforms.

How to tell if an animation finished completely, or was interrupted?

I have a UIView and two UISwipeGesture classes that animate the X rotation between 0 and -99, giving the flip board effect. If the user swipes down and then immediately swipes back up, the 'swipe down' animation is ended prematurely and it begings the swipe up animation.
How can I tell if it gets ended prematurely due to another animation being added? The animationDidStop:finished message gets sent, but the finished value is always TRUE.
Here's swipe down code:
CATransform3D transform = CATransform3DIdentity;
// This must be set before we calculate the transforms to give the 3D perspective (1.0 / -DISTANCE_FROM_CAMERA)
transform.m34 = 1.0 / -4000;
// Rotate on the X axis
transform = CATransform3DRotate(transform, DegreesToRadians(-99), 1, 0, 0);
// Apply transform in an animation
CABasicAnimation* foldDownAnimatnion = [CABasicAnimation animationWithKeyPath:#"transform"];
foldDownAnimatnion.duration = 1;
foldDownAnimatnion.toValue = [NSValue valueWithCATransform3D:transform];
foldDownAnimatnion.removedOnCompletion = NO;
foldDownAnimatnion.fillMode = kCAFillModeForwards;
foldDownAnimatnion.delegate = self;
// Identify this animation in delegate method
[foldDownAnimatnion setValue:#"foldDown" forKey:#"name"];
[foldDownAnimatnion setValue:theLayer forKey:#"layer"];
[theLayer addAnimation:foldDownAnimatnion forKey:nil];
And my delegate method:
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished
{
if([[animation valueForKey:#"name"] isEqualToString:#"foldDown"])
{
// Why is this always YES??
NSLog(#"Animation finished: %#", (finished)?#"Yes" : #"No");
}
else if([[animation valueForKey:#"name"] isEqualToString:#"foldUp"])
{
NSLog(#"animationDidStop: foldUp");
}
}
Add two BOOL instance variable(BOOL UpAnimationStarted; BOOL DownAnimationStarted;) In the beginning of SwipeUp set UpAnimationStarted to YES. And In the beginning of SwipeDown set DownAnimationStarted to YES. You can use these BOOL values to check animation interruption.
Thanks to #DavidRönnqvist for pointing me in the right direction, I went back to my O'Reilly book on iOS4 and redid the animation, the code now much cleaner:
Swipe Down:
CATransform3D transform = CATransform3DIdentity;
// This must be set before we calculate the transforms to give the 3D perspective (1.0 / -DISTANCE_FROM_CAMERA)
transform.m34 = 1.0 / -4000;
// Rotate on the X axis
transform = CATransform3DRotate(transform, DegreesToRadians(-99), 1, 0, 0);
// Apply transform in an animation
[CATransaction setDisableActions:YES];
theLayer.transform = transform;
CABasicAnimation* foldDownAnimatnion = [CABasicAnimation animationWithKeyPath:#"transform"];
foldDownAnimatnion.duration = 1;
foldDownAnimatnion.delegate = self;
[foldDownAnimatnion setValue:#"foldDown" forKey:#"name"];
[foldDownAnimatnion setValue:theLayer forKey:#"layer"];
[theLayer addAnimation:foldDownAnimatnion forKey:#"foldDown"];
Swipe Up
[theLayer removeAnimationForKey:#"foldDown"];
CATransform3D transform = CATransform3DIdentity;
// This must be set before we calculate the transforms to give the 3D perspective (1.0 / -DISTANCE_FROM_CAMERA)
transform.m34 = 1.0 / -4000;
// Rotate on the X axis
transform = CATransform3DRotate(transform, DegreesToRadians(0), 1, 0, 0);
// Apply transform in an animation
[CATransaction setDisableActions:YES];
theLayer.transform = transform;
CABasicAnimation* animatnion = [CABasicAnimation animationWithKeyPath:#"transform"];
animatnion.duration = 1;
animatnion.delegate = self;
[animatnion setValue:#"foldUp" forKey:#"name"];
[theLayer addAnimation:animatnion forKey:#"foldUp"];
The key really is that I am applying the new transform to the layer, and then animating it. What I was doing before was applying the animation to the layer and making it fill forwards, this caused problems when I removed it mid-way.

Animate UIImage in UIImageView Up & Down (Like it's hovering) Loop

Hello I have an image that I'd like to move up and down (Up 10 pixels and down 10 pixels) so that my image appears to be hovering. How can I do this with simple animation thanks so much!!!
You could use Core Animation to animate the position of the views layer. If you configure the animation to be additive you won't have to bother calculating the new absolute position, just the change (relative position).
CABasicAnimation *hover = [CABasicAnimation animationWithKeyPath:#"position"];
hover.additive = YES; // fromValue and toValue will be relative instead of absolute values
hover.fromValue = [NSValue valueWithCGPoint:CGPointZero];
hover.toValue = [NSValue valueWithCGPoint:CGPointMake(0.0, -10.0)]; // y increases downwards on iOS
hover.autoreverses = YES; // Animate back to normal afterwards
hover.duration = 0.2; // The duration for one part of the animation (0.2 up and 0.2 down)
hover.repeatCount = INFINITY; // The number of times the animation should repeat
[myView.layer addAnimation:hover forKey:#"myHoverAnimation"];
Since this is using Core Animation you will need to add QuartzCore.framework and #import <QuartzCore/QuartzCore.h> into your code.
For Swift 3, Swift 4 and Swift 5:
let hover = CABasicAnimation(keyPath: "position")
hover.isAdditive = true
hover.fromValue = NSValue(cgPoint: CGPoint.zero)
hover.toValue = NSValue(cgPoint: CGPoint(x: 0.0, y: 100.0))
hover.autoreverses = true
hover.duration = 2
hover.repeatCount = Float.infinity
myView.layer.add(hover, forKey: "myHoverAnimation")
Try this:
CGRect frm_up = imageView.frame;
frm_up.origin.y -= 10;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeats
animations:^{
imageView.frame = frm_up;
}
completion:NULL
];
For such a task, it will be better to place an image in a sublayer of UIView, and then animate that sublayer frame. This tutorial could give you some idea: http://www.raywenderlich.com/2502/introduction-to-calayers-tutorial

Resources