31

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

tobinjim
  • 1,852
  • 1
  • 19
  • 30
  • I'm just realizing that my basic understanding of the CABasicAnimation was a little flawed, although I don't think that bears on the result I'm trying to achieve. The transform created by the CATransform3DRotate call is the end result at the end of the CABasicAnimation. I was thinking of it as sort of a function into which time slices were passed and used to compute the time-slice rotation. In fact, the CABasicAnimation is interpolating the in-between steps needed to achieve the end result. Which means I shouldn't be able to do this in one transform/animation I fear. – tobinjim Feb 24 '12 at 01:11

2 Answers2

63

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

rob mayoff
  • 342,380
  • 53
  • 730
  • 766
  • Rob, thank you very much. I had gone to bed, but as happens all too often, the brain wouldn't shut down. I came back to my computer to try a variation on tommyli's suggestion but saw your spinning record .gif! Your answer is very clear. I had not seen a description of how to get the layer of a view to be a CATransformLayer before; perhaps due to searching for CATransform3D. Nonetheless, in my book you get credit for showing at least me how to use it properly! – tobinjim Feb 24 '12 at 09:45
  • Tickled! :-) I had to set the zPosition as mentioned elsewhere so the disc didn't disappear behind my background as it rotated into -z space, and increased the perspective divisor, but it looks great now. Thanks!!! – tobinjim Feb 24 '12 at 10:14
  • @Rob can you convert this to OpenGL ? Actually i need to take screen shot of CATranform3D layer. But it can't work without opengl. please suggest. – Muzammil Apr 19 '12 at 09:46
  • 23
    I am not interested in rewriting this using OpenGL. – rob mayoff Apr 19 '12 at 16:48
  • the test project link is disappear . would you please send me a demo ? thanks very much – ShineWang Jan 07 '16 at 09:54
  • @ShineWang I've updated my answer with a working Swift playground. – rob mayoff Jan 07 '16 at 15:47
  • say @robmayoff. I've been wondering how one can actually move the axis of rotation in CATransform3DRotate ... http://stackoverflow.com/questions/37364831 . Perhaps you know! – Fattie May 21 '16 at 17:16
0

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.

tommyli
  • 131
  • 6
  • Thanks, tommyli, and if what I implemented had worked out the way I think intended, it would have been close. I substituted y-axis for your recommendation of z-axis, since the superlayer hadn't been transformed. I was expecting the "spinning nickel" effect, where the "raised" back would rotate around and be in front (and then continue on around to the back). But all I'm really seeing is rotation around the z-axis anyway. – tobinjim Feb 24 '12 at 04:35