Core Animation 三 : CALayer的仿射变换,

2020-12-31  本文已影响0人  Trigger_o

什么是仿射变换
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
    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来布局.

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

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

对于这个正方体,如果想在它转动的时候不会看起来一会儿远一会儿近,就应该把正方体的中心放在坐标原点(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
上一篇下一篇

猜你喜欢

热点阅读