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

17,671

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:

spinning disc

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:

playground result

Share:
17,671
tobinjim
Author by

tobinjim

Updated on June 20, 2022

Comments

  • tobinjim
    tobinjim about 2 years

    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:

    Gold Record

    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).