Origami transition using CATransform3D perspective

狂风中的少年 提交于 2020-01-22 04:32:07

问题


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];
}

回答1:


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];
}



回答2:


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



回答3:


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.




回答4:


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.




回答5:


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


来源:https://stackoverflow.com/questions/5456642/origami-transition-using-catransform3d-perspective

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!