什么是仿射變換
CALayer的變換和Core Graphics變換沒什么區(qū)別,本篇主要講一些關于3D變換的內(nèi)容.
3D變換的平移和縮放與平面的變換完全一樣,只是增加一個坐標軸的參數(shù),但是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);
所以CGAffineTransformRotate這個方法是以z軸為中心旋轉,如果不是的話,就需要增加參數(shù)了
/* 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個參數(shù),并且不能都為0,因為需要一個向量作為軸,上面這個就是以(1,0,0)為軸,實質就是x軸.
為了看起來方便,加了個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的結構和單位向量;再提一下,這個和前面的什么是仿射變換,里面略有區(qū)別,里面的齊次坐標把點定義為單列矩陣(或者說是列向量),而Apple把點定義為單行矩陣(行向量);
想象一下帶有透視的旋轉,它的投影應該是一個梯形,也就是每個點的x坐標是要發(fā)生變化的,這里直接說結論,最簡單的方法是在旋轉變換前,先修改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之后再旋轉,現(xiàn)在就有了透視效果
可以打印出變換矩陣看一看,和單位矩陣對比,發(fā)現(xiàn)有哪些元素發(fā)生了變化
但是這里m34的值還是有問題,后面說明
那么為什么m34和m24會影響到變換的結果,通過矩陣乘法能看出來明明xyz的值與最右邊一列沒有關系
這又得重新說到齊次坐標,幫助理解齊次坐標
在二維變換中,齊次坐標右下角的元素經(jīng)常默認是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發(fā)生變化時,變換后的m44就不是原來的m44了,所以變換后的w也不再是原來的w(如果一開始m44是1的話,w也是1),x和y也就發(fā)生了變化.
修改m34 -> 點(x,y,1)變換 -> m44變成w -> m44改成1 -> 變換后的點(x/w,y/w,1)
t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);
根據(jù)上面的理解,所以即便改成y軸旋轉也是修改m34,繞z軸旋轉就是平面變換了,m34不會影響結果
t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);
x軸旋轉之后再y軸旋轉
-
關于滅點
滅點就是透視效果的視野消失點,在iOS中,它就是layer的錨點anchorPoint
所以修改錨點會對透視效果產(chǎn)生影響,想要平移圖形應該使用變換而不是修改錨點和position
下面兩張圖,再沒有變換的時候,都是居中的,同樣的變換,因為anchorPoint不同效果也不同
-
sublayerTransform
這個屬性是將子視圖的變換同步起來,它通常是使用在統(tǒng)一子layer的滅點
例如,一個大layer A上有一個小的layer a,A的錨點是(0,0),a的錨點是自己的中心
當a設置了m34然后做3d變換之后是這樣的,圖1
如果設置了A的sublayerTransform,然后a不再設置m34,直接做變換,是這樣的,圖2
CATransform3D t = CATransform3DIdentity;
A.m34 = .0015f;
A.sublayerTransform = t;
這樣,子layer的滅點就統(tǒng)一成了父layer的滅點,如果有很多個子layer的話,所有的子layer都統(tǒng)一了透視.
此時打印a的anchorPoint,發(fā)現(xiàn)還是0.500000,0.500000,完全沒變,也就是說,設置了sublayerTransform之后,子layer的anchorPoint不再影響滅點,就可以隨意的使用anchorPoint和position或者frame來布局.
這個屬性非常強大,后面的例子會繼續(xù)說明.
-
layer的背面
如果一個layer做3d變換,如果轉到背面去了,會發(fā)生翻轉
如果設置doubleSided = NO;就什么都看不到了,甚至gpu都不會去執(zhí)行繪制.
下面通過一個demo來說明上面的內(nèi)容
- 目標是繪制一個正方體,每個面都區(qū)分開來,實現(xiàn)透視效果,滑動屏幕可以使正方體轉動
想象一下,用6個視圖來構建正方體,想要正方體轉動,每個面都有不同的變換,及其繁瑣,這里就需要sublayerTransform出場了,6個面處在同一視覺系統(tǒng)內(nèi),有同一個滅點,正方體的立體效果就實現(xiàn)了.
同時,當進行變換的時候,修改sublayerTransform,六個面都會發(fā)生變化;
對于這個正方體,如果想在它轉動的時候不會看起來一會兒遠一會兒近,就應該把正方體的中心放在坐標原點(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);
結果發(fā)現(xiàn)反而1小,6大,這里就是之前說的,想要達到合適的透視效果,m34其實應該是負值
這樣就對了,不過還有一個問題,第六個面并不是簡單的往后移動100單位就行了,它其實應該是翻轉180度,假如layer1有不透明度,我們看到的應該是layer6的背面,再假如如果layer6把doubleSided設置為NO,那我們應該看不到layer6.
t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);
所以這里應該翻轉layer6
說明了這些問題之后,直接上代碼就可以了
@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里的內(nèi)容
手指滑動時,bottomLayer圍繞currentPoint和self.touchPoint組成的向量轉動;delta是手指滑動的距離
6個面通過自身的變換,在空間上形成正方體,正方體的轉動通過bottomLayer的sublayerTransform來實現(xiàn).
最終效果如下