1. 平移(Translation)
给二维空间坐标引入第 3 个分量 w,使平移变换也能表示为矩阵乘法,从而将所有几何变换统一为矩阵运算。
平移变换将点 (x,y) 移动到 (x+tx,y+ty):
x′y′=x+tx=y+ty⇒x′y′1=100010txty1xy1
T(tx,ty)=100010txty1
2. 缩放(Scale)
缩放变换将点 (x,y) 缩放到 (sx⋅x,sy⋅y):
x′y′=sx⋅x=sy⋅y⇒x′y′1=sx000sy0001xy1
S(sx,sy)=sx000sy0001
2.1 反射(Reflection)
反射是缩放的特例。当 sx 或 sy 取 −1 时,产生关于坐标轴的镜像:
- 水平翻转(关于 Y 轴):sx=−1, sy=1
S(−1,1)=−100010001
- 竖直翻转(关于 X 轴):sx=1, sy=−1
S(1,−1)=1000−10001
3. 旋转(Rotation)
绕原点逆时针旋转 θ:
x′y′=xcosθ−ysinθ=xsinθ+ycosθ⇒x′y′1=cosθsinθ0−sinθcosθ0001xy1
R(θ)=cosθsinθ0−sinθcosθ0001
旋转矩阵是正交矩阵,满足 R−1(θ)=RT(θ)=R(−θ)。
4. 错切(Shear)
错切变换使图形沿某一坐标轴方向发生"倾斜",保持面积不变。
- 水平错切:沿 X 轴方向偏移,偏移量与 y 成正比
Hx(k)=100k10001⇒{x′=x+k⋅yy′=y
- 竖直错切:沿 Y 轴方向偏移,偏移量与 x 成正比
Hy(k)=1k0010001⇒{x′=xy′=y+k⋅x
5. 复合变换
多个变换可以通过矩阵乘法组合为一个变换:
v′=Tn⋯T2T1v
矩阵乘法从右向左依次作用。即先应用 T1,再应用 T2,最后应用 Tn。
例如,先缩放再旋转再平移(TRS 组合)是图形学中最常见的复合变换:
M=T(tx,ty)⋅R(θ)⋅S(sx,sy)
6. 翻转变换

- UI坐标:原点在左上角,X 轴向右,Y轴向下,如UIKit View坐标,图像像素等
- 笛卡尔坐标:原点在左下角,X 轴向右,Y轴向上,如OpenGL、Metal、Quartz 2D等
x′y′1=1000−100h1xy1
点的空间位置不变,只是换了一个坐标系来描述它,即移动旧坐标系的基,使其与新坐标系对齐,这里先将 Y 轴反向,再将原点移到左下角。
注意,变换的每一步都是相对原坐标系进行的:旋转和缩放始终以原点为基准,且原点位置始终不变(这里是左上角)。因此先向上平移 T(0,−h) 再翻转也是可以的,结果同样是 y′=−y+h。
M=T(0,h)⋅S(1,−1)=1000100h11000−10001=1000−100h1
反过来:
M−1=S(1,−1)⋅T(0,−h)=1000−100011000100−h1=1000−100h1
两个方向的变换矩阵恰好相同,而这个矩阵恰好是自逆的(M2=I)。
7. 实际应用
7.1 Quartz绘图时的坐标变换
在iOS开发时,通常使用 Core Graphics(Quartz)进行绘图操作,需要使用CGAffineTransform处理坐标变换。
public struct CGAffineTransform {
public init()
public init(a: Double, b: Double, c: Double, d: Double, tx: Double, ty: Double)
public var a: Double
public var b: Double
public var c: Double
public var d: Double
public var tx: Double
public var ty: Double
}
struct CGAffineTransform {
CGFloat a, b, c, d;
CGFloat tx, ty;
};
CGAffineTransform 本质上就是 3×3 齐次坐标矩阵的前两列(因为第三列恒为 (0,0,1)):
M=actxbdty001
- Core Graphics使用行向量表示坐标,即 P′=PM
- CGAffineTransformConcat(M1, M2)的效果是M′=M1∗M2,表示的是矩阵的乘法
- CGAffineTransformTranslate/Scale/Rotate考虑了作用顺序的问题,例如Translate的效果是 M′=T(tx,ty)∗Min,相当于Concat(T(tx,ty),Min)
该图分几步绘制而成,实现逻辑如下:
override func draw(_ rect: CGRect) {
let w = rect.width
let h = rect.height
UIColor(white: 0.9, alpha: 1.0).setFill()
UIGraphicsGetCurrentContext()?.fill(rect)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
drawAxes(in: ctx, color: .yellow)
ctx.translateBy(x: floor(w / 4), y: floor(h / 4))
drawAxes(in: ctx, color: .red)
ctx.rotate(by: .pi / 6.0)
ctx.translateBy(x: 115, y: 0.0)
ctx.scaleBy(x: 0.5, y: 0.5)
drawAxes(in: ctx, color: .green.withAlphaComponent(0.5))
}
- (void)drawRect:(CGRect)rect {
CGFloat w = CGRectGetWidth(rect);
CGFloat h = CGRectGetHeight(rect);
[[UIColor colorWithWhite:0.9 alpha:1.0] set];
CGContextFillRect(UIGraphicsGetCurrentContext(), rect);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self drawAxesIn:ctx color:UIColor.yellowColor];
CGContextTranslateCTM(ctx, floor(w/4), floor(h/4));
[self drawAxesIn:ctx color:UIColor.redColor];
CGContextRotateCTM(ctx, M_PI / 6.0);
CGContextTranslateCTM(ctx, 115, 0.0);
CGContextScaleCTM(ctx, 0.5, 0.5);
[self drawAxesIn:ctx color:[UIColor.greenColor colorWithAlphaComponent:0.5]];
}
假设屏幕scale是3,当前view的大小是(300,300)。
M1=1000−1300001×300030001=3000−3900001
在调用drawRect前,UIKit会对CTM进行如下处理:
- 点到像素的缩放变换
- Y轴翻转变换
- 绘制红色坐标轴时的CTM
M2=10750175001×3000−3900001=302250−3675001
在前一步的基本上对CTM进行一个平移变换
- 绘制绿色坐标轴时的CTM
M3=0.50000.50001×10115010001×cos6π−sin6π0sin6πcos6π0001×M2=1.299038−0.75523.7788−0.75−1.299038502.5001
在前一步的基础上进行旋转、平移、缩放
每一次对CTM的操作,都可以看到对绘图的局部坐标系进行了变换。
7.2 UIView/CALayer的坐标变换
UIView 与坐标变换有关的属性:
transform :类型是 CGAffineTransform,对应二维仿射变换,是三维变换的子集
transform3D:类型是 CATransform3D,对应三维变换
anchorPoint:类型是 CGPoint,变换的基准点
CALayer与坐标变换有关的属性:
affineTransform:类型是CGAffineTransform,对应二维仿射变换,是三维变换的子集
transform类型是 CATransform3D,对应三维变换
anchorPoint:类型是 CGPoint,变换的基准点
UIKit坐标体系中的Y轴是向下的,即相对于笛卡尔坐标系存在一个Y的翻转,因此旋转是顺时针的。