美文网首页
Core Animation 三 : CALayer的仿射变换,

Core Animation 三 : CALayer的仿射变换,

作者: Trigger_o | 来源:发表于2020-12-31 15:29 被阅读0次

    什么是仿射变换
    CALayer的变换和Core Graphics变换没什么区别,本篇主要讲一些关于3D变换的内容.

    3D变换的平移和缩放与平面的变换完全一样,只是增加一个坐标轴的参数,但是3D的旋转就不一样了,重点来看这个

    CAGradientLayer *gl = [CAGradientLayer layer];
        gl.startPoint = CGPointMake(0, 0);
        gl.endPoint = CGPointMake(1, 1);
        gl.colors = @[(__bridge id)UIColor.redColor.CGColor,(__bridge id)UIColor.orangeColor.CGColor,(__bridge id)UIColor.yellowColor.CGColor];
        gl.locations = @[@0.2,@.6,@1.0];
        gl.type = kCAGradientLayerAxial;
        gl.frame = (CGRect){75,100,ScreenWidth-150, ScreenWidth-150};
        [self.view.layer addSublayer:gl];
    
    初始状态

    3D视角下Z轴是垂直于屏幕的,不管是2d变换还是3d变换,其实z轴是一直存在的,
    想象一下,2d变换的旋转,就是Z轴位中心进行旋转,并且是以自身的中心为旋转中心,即layer的中心是三维坐标轴的(0,0,0),与anchorPoint锚点无关,不管锚点在哪,原点都在layer的中心.
    也就是说,可以认为是围绕一个(0,0,1)的单位向量旋转的,当然也可以是(0,0,-1)

    gl.affineTransform = CGAffineTransformRotate(gl.affineTransform, M_PI_4);
    
    2d旋转

    所以CGAffineTransformRotate这个方法是以z轴为中心旋转,如果不是的话,就需要增加参数了

    /* Rotate 't' by 'angle' radians about the vector '(x, y, z)' and return
     * the result. If the vector has zero length the behavior is undefined:
     * t' = rotation(angle, x, y, z) * t. 
    
    CA_EXTERN CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle,
        CGFloat x, CGFloat y, CGFloat z)
        API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    */
    gl.transform = CATransform3DRotate(gl.transform, -M_PI_4, 1, 0, 0);
    gl.contents = (id)[UIImage imageNamed:@"avatar"].CGImage; 
    

    所以CATransform3DRotate除了角度之外还有3个参数,并且不能都为0,因为需要一个向量作为轴,上面这个就是以(1,0,0)为轴,实质就是x轴.


    image.png

    为了看起来方便,加了个contents,结果看起来只是变的扁了,这是因为变换本身并没有透视效果,没有近大远小,当围绕x轴旋转时,远离我们的一端应该看起来更小,靠近的一端应该看起来更大;

    struct CATransform3D
    {
      CGFloat m11, m12, m13, m14;
      CGFloat m21, m22, m23, m24;
      CGFloat m31, m32, m33, m34;
      CGFloat m41, m42, m43, m44;
    };
    /* The identity transform: [1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]. */
    

    这是CATransform3D的结构和单位向量;再提一下,这个和前面的什么是仿射变换,里面略有区别,里面的齐次坐标把点定义为单列矩阵(或者说是列向量),而Apple把点定义为单行矩阵(行向量);
    想象一下带有透视的旋转,它的投影应该是一个梯形,也就是每个点的x坐标是要发生变化的,这里直接说结论,最简单的方法是在旋转变换前,先修改m34的值

        CATransform3D t = gl.transform;
        t.m34 = .0015f;
        t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
        //t = (CATransform3D){t.m11,t.m12,t.m13,t.m14,t.m21,t.m22,t.m23,-0.001,t.m31,t.m32,t.m33,0.001,t.m41,t.m42,t.m43,t.m44};
        gl.transform = t;
    

    设置了m34之后再旋转,现在就有了透视效果


    修改m34之后再旋转

    可以打印出变换矩阵看一看,和单位矩阵对比,发现有哪些元素发生了变化


    没有经过变换的单位矩阵
    旋转矩阵
    修改m34之后再旋转的矩阵

    但是这里m34的值还是有问题,后面说明

        那么为什么m34和m24会影响到变换的结果,通过矩阵乘法能看出来明明xyz的值与最右边一列没有关系
    

    这又得重新说到齐次坐标,帮助理解齐次坐标
    在二维变换中,齐次坐标右下角的元素经常默认是1,而点的矩阵是(x,y,1),但是实际上它可以是任意值w,而点的矩阵变成了(x/w,y/w,1);这么一来,w直接影响到整个视图的比例,如果设置m44是2,则相当于做了一次缩小,宽高都变成原来的1/2,在iOS中,平面的仿射变换CGAffineTransform定义为6个值,也就是忽略了齐次坐标增加的一列,这一列在iOS中默认为(0,0,1),但是CATransform3D是16个值,m44是可以不为1的;
    还没完,除了m44可以不是1之外,x和y也不是看起来的x和y,因为在3d变换中,最终的显示效果都是光栅化的结果,也就是在xy平面的投影,当m34发生变化时,变换后的m44就不是原来的m44了,所以变换后的w也不再是原来的w(如果一开始m44是1的话,w也是1),x和y也就发生了变化.
    修改m34 -> 点(x,y,1)变换 -> m44变成w -> m44改成1 -> 变换后的点(x/w,y/w,1)

    t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);
    

    根据上面的理解,所以即便改成y轴旋转也是修改m34,绕z轴旋转就是平面变换了,m34不会影响结果


    绕y轴旋转
    t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
    t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);
    

    x轴旋转之后再y轴旋转


    image.png
    • 关于灭点
      灭点就是透视效果的视野消失点,在iOS中,它就是layer的锚点anchorPoint


      image.png

      所以修改锚点会对透视效果产生影响,想要平移图形应该使用变换而不是修改锚点和position
      下面两张图,再没有变换的时候,都是居中的,同样的变换,因为anchorPoint不同效果也不同


      anchorPoint是(.5,.5)
      anchorPoint是(0,0)
    • sublayerTransform
      这个属性是将子视图的变换同步起来,它通常是使用在统一子layer的灭点
      例如,一个大layer A上有一个小的layer a,A的锚点是(0,0),a的锚点是自己的中心
      当a设置了m34然后做3d变换之后是这样的,图1


      图1

      如果设置了A的sublayerTransform,然后a不再设置m34,直接做变换,是这样的,图2

        CATransform3D t = CATransform3DIdentity;
        A.m34 = .0015f;
        A.sublayerTransform = t;
    
    图2

    这样,子layer的灭点就统一成了父layer的灭点,如果有很多个子layer的话,所有的子layer都统一了透视.

    此时打印a的anchorPoint,发现还是0.500000,0.500000,完全没变,也就是说,设置了sublayerTransform之后,子layer的anchorPoint不再影响灭点,就可以随意的使用anchorPoint和position或者frame来布局.

    这个属性非常强大,后面的例子会继续说明.

    • layer的背面
      如果一个layer做3d变换,如果转到背面去了,会发生翻转


      image.png

      如果设置doubleSided = NO;就什么都看不到了,甚至gpu都不会去执行绘制.

    下面通过一个demo来说明上面的内容

    • 目标是绘制一个正方体,每个面都区分开来,实现透视效果,滑动屏幕可以使正方体转动
      想象一下,用6个视图来构建正方体,想要正方体转动,每个面都有不同的变换,及其繁琐,这里就需要sublayerTransform出场了,6个面处在同一视觉系统内,有同一个灭点,正方体的立体效果就实现了.
      同时,当进行变换的时候,修改sublayerTransform,六个面都会发生变化;

    对于这个正方体,如果想在它转动的时候不会看起来一会儿远一会儿近,就应该把正方体的中心放在坐标原点(0,0,0)
    正方体六个面大小都是(200,200),它的大小本身不会变化,但是当设置了m34之后,z值就会影响它在视觉上的大小.
    首先第一个面,离我们最近,在z轴正方向设置为100;第六个面离我们最远,z是-100,看看效果

    CATransform3D subt = CATransform3DIdentity;
    subt.m34 = .0015f;
    self.bottomLayer.sublayerTransform = subt;
    CATransform3D t6 = CATransform3DMakeTranslation(0, 0, -100);
    CATransform3D t1 = CATransform3DMakeTranslation(0, 0, 100);
    
    image.png

    结果发现反而1小,6大,这里就是之前说的,想要达到合适的透视效果,m34其实应该是负值


    m34改成负值

    这样就对了,不过还有一个问题,第六个面并不是简单的往后移动100单位就行了,它其实应该是翻转180度,假如layer1有不透明度,我们看到的应该是layer6的背面,再假如如果layer6把doubleSided设置为NO,那我们应该看不到layer6.

    t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);
    

    所以这里应该翻转layer6


    翻转
    layer6关闭doubleSided就看不到了

    说明了这些问题之后,直接上代码就可以了

    @property (nonatomic, strong) CALayer *bottomLayer;
    @property (nonatomic, strong) CALayer *layer1;
    @property (nonatomic, strong) CALayer *layer2;
    @property (nonatomic, strong) CALayer *layer3;
    @property (nonatomic, strong) CALayer *layer4;
    @property (nonatomic, strong) CALayer *layer5;
    @property (nonatomic, strong) CALayer *layer6;
    @property (nonatomic, assign) CGPoint touchPoint;
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
       
        self.bottomLayer = [CALayer layer];
        self.bottomLayer.frame = CGRectMake(0, 100, ScreenWidth, ScreenWidth);
        self.bottomLayer.backgroundColor = [[UIColor hex:@"cccccc"] colorWithAlphaComponent:.7].CGColor;
        [self.view.layer addSublayer:self.bottomLayer];
        
        CATransform3D subt = CATransform3DIdentity;
        subt.m34 = -.0015f;
        self.bottomLayer.sublayerTransform = subt;
        
        CATransform3D t6 = CATransform3DMakeTranslation(0, 0, -100);
        t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);
        self.layer6 = [self createLayer:6 color:[UIColor hex:@"A0522D"] transform:t6];
        
        CATransform3D t5 = CATransform3DMakeTranslation(-100, 0, 0);
        t5 = CATransform3DRotate(t5, -M_PI_2, 0, 1, 0);
        self.layer5 = [self createLayer:5 color:[UIColor hex:@"DAA520"] transform:t5];
    
        CATransform3D t4 = CATransform3DMakeTranslation(0, 100, 0);
        t4 = CATransform3DRotate(t4, -M_PI_2, 1, 0, 0);
        self.layer4 = [self createLayer:4 color:[UIColor hex:@"228B22"] transform:t4];
    
        CATransform3D t3 = CATransform3DMakeTranslation(0, -100, 0);
        t3 = CATransform3DRotate(t3, M_PI_2, 1, 0, 0);
        self.layer3 = [self createLayer:3 color:[UIColor hex:@"5F9EA0"] transform:t3];
    
        CATransform3D t2 = CATransform3DMakeTranslation(100, 0, 0);
        t2 = CATransform3DRotate(t2, M_PI_2, 0, 1, 0);
        self.layer2 = [self createLayer:2 color:[UIColor hex:@"4682B4"] transform:t2];
        
        CATransform3D t1 = CATransform3DMakeTranslation(0, 0, 100);
        self.layer1 = [self createLayer:1 color:[UIColor hex:@"708090"] transform:t1];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.touchPoint = CGPointZero;
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        UITouch *touch = [touches anyObject];
        CGPoint currentPoint = [touch locationInView:self.view];
        if(CGPointEqualToPoint(self.touchPoint, CGPointZero)){
            self.touchPoint = currentPoint;
        }
        CGFloat deltax = currentPoint.x - self.touchPoint.x;
        CGFloat deltay = currentPoint.y - self.touchPoint.y;
        CGFloat delta = sqrt(pow((self.touchPoint.y - currentPoint.y), 2) + pow((self.touchPoint.x - currentPoint.x), 2));
        self.touchPoint = currentPoint;
        CATransform3D subt = self.bottomLayer.sublayerTransform;
        subt = CATransform3DRotate(subt, M_PI/360.0*delta, -deltay, deltax, 0);
        self.bottomLayer.sublayerTransform = subt;
    }
    
    - (CALayer *)createLayer:(NSInteger)index color:(UIColor *)color transform:(CATransform3D)transform{
        CALayer *layer = [CALayer layer];
        CGFloat wh = 200;
        CGFloat xy = (ScreenWidth - 200)/2;
        CGRect frame = CGRectMake(xy, xy, wh, wh);
        layer.frame = frame;
        layer.backgroundColor = [color colorWithAlphaComponent:.3].CGColor;
        [self.bottomLayer addSublayer:layer];
    //    layer.doubleSided = NO;
        layer.transform = transform;
        
        CATextLayer *tl = [CATextLayer layer];
        tl.contentsScale = UIScreen.mainScreen.scale;
        tl.alignmentMode = kCAAlignmentCenter;
        tl.doubleSided = NO;
        UIFont *f = [UIFont systemFontOfSize:100 weight:UIFontWeightBold];
        NSMutableAttributedString *att = [[NSMutableAttributedString alloc]initWithString:[NSString stringWithFormat:@"%ld",index]];
        NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
        [att addAttributes:@{NSFontAttributeName:f,NSForegroundColorAttributeName:UIColor.whiteColor,NSParagraphStyleAttributeName:paragraph} range:NSMakeRange(0, att.length)];
        tl.string = att;
        tl.position = CGPointMake(wh/2, wh/2);
        tl.bounds = CGRectMake(0, 0, f.lineHeight, f.lineHeight);
        [layer addSublayer:tl];
        return layer;
    }
    
    

    解释下touchesMoved里的内容
    手指滑动时,bottomLayer围绕currentPoint和self.touchPoint组成的向量转动;delta是手指滑动的距离
    6个面通过自身的变换,在空间上形成正方体,正方体的转动通过bottomLayer的sublayerTransform来实现.
    最终效果如下

    doubleSided YES doubleSided NO

    相关文章

      网友评论

          本文标题:Core Animation 三 : CALayer的仿射变换,

          本文链接:https://www.haomeiwen.com/subject/isyngktx.html