转载自:http://www.jianshu.com/p/62d99d7f1b7a
这BGM有毒 : My Songs Know What You Did In The Dark
Quartz作为iOS和OS平台上的绘图引擎,功能不可谓不强大,强大的同时意味着相对复杂,刚上手时可能晦涩难懂。这篇文章结合了 官方文档 以及自己的理解进行补充、总结。内容可能涉及一些初等、高等数学和一些图形学知识,花了一些时间整理,篇幅原因,只贴关键代码。最近皮神火了,试着随性山寨一下,很多地方偷懒了,弄了一个大概,画个皮神足矣。
1798073-23321bb0fa3263b6.PNG
Graphics Contexts
图形上下文包含很多有用信息,其中包括绘图参数以及所有设备信息,并且决定绘制到什么输出设备上
C4B4CBC9-FE17-4FA2-A590-044A81538134.png
-
获得基于图层的图形上下文
基于图层的图形上下文通过系统创建,而不是我们自己创建,我们要做的仅仅是获得。当视图显示在屏幕上,或者它的内容需要重绘时,UIView的drawRect:方法被调用,在这之前,视图对象自动配置绘图环境,作为配置的一部分,UIView对象对于当前绘图环境创建图形上下文,我们通过
UIGraphicsGetCurrentContext
获得。当然,我们也可以通过layer的代理方法drawLayer:inContext:方法或者重写layer的drawInContext:方法直接获得图形上下文,毕竟真正操作的始终是图层,drawRect:方法只是苹果提供给面向视图开发的一个接口。layer的方法就此打住。 -
创建基于位图的图形上下文
基于位图的图形上下文通过
UIGraphicsBeginImageContextWithOptions
创建,UIGraphicsGetCurrentContext
获得,UIGraphicsEndImageContext
清除绘图环境。
Paths
创建路径
任意时间,一个图形上下文只能有一条在使用的路径(当前路径),但一条路径可以包含多条子路径
A graphics context can have only a single path in use at any time.
-
CGContextBeginPath
开启一条新的空路径作为当前路径,如果上下文已经包含一个当前路径,那么Quartz移除老的路径以及和它相关的任何数据
子路径
子路径可以是线,圆弧,曲线,圆,矩形或者这些组合等等。
-
CGContextMoveToPoint
在指定点开启一条新的子路径,该点作为子路径的起点,当前点被设置为子路径的起点
-
CGContextAddLineToPoint
需要当前路径非空。如果当前路径为空,那么调用CGContextMoveToPoint
-
CGContextAddLines
在数组第一个点开启一条新的子路径,相当于 :
CGContextMoveToPoint (c, points[0].x, points[0].y);
for (k = 1; k < count; k++) {
CGContextAddLineToPoint (c, points[k].x, points[k].y);
}
-
CGContextAddArcToPoint
如果当前路径为空,那么在圆弧的起点开启一条新的子路径。如果当前路径非空,那么Quartz从当前点到圆弧的起点追加一条线
-
CGContextAddQuadCurveToPoint
二次贝塞尔曲线,需要当前路径非空。一共3个点,其中一个控制点,需要进行两轮取点操作
Bézier_2_big.gif
-
CGContextAddCurveToPoint
三次贝塞尔曲线,需要当前路径非空。一共4个点,其中两个控制点,需要进行三轮取点操作
Bézier_3_big.gif
-
CGContextClosePath
闭合当前路径的子路径。Quartz从当前点到子路径起点追加一条线,当前点被设置为子路径的起点
注 : 闭合当前子路径,应该调用CGContextClosePath。终点和起点重合的直线,圆弧和曲线不会自动闭合子路径,必须显示调用CGContextClosePath闭合子路径
To close the current subpath, your application should call CGContextClosePath. This function adds a line segment from the current point to the starting point of the subpath and closes the subpath. Lines, arcs, and curves that end at the starting point of a subpath do not actually close the subpath. You must explicitly call CGContextClosePath to close a subpath.
先介绍一些图形状态
-
CGContextSetLineWidth
线宽,没什么好说的
-
CGContextSetLineCap
线段端点的风格
73FC2057-B171-43D8-80EB-1C2124E4434F.png
-
CGContextSetLineJoin
线段连接点的风格
F74D6BC9-65E6-4784-9281-B7512FAE49E5.png
-
CGContextSetLineDash
虚线模式
CGFloat const lengths [] = {10.0, 2.0};
/**
* CGContextSetLineDash 虚线模式
* 当CGLineCap为kCGLineCapButt时才有效
*
* @param phase 距离多远开始绘制
* @param lengths 数组,表示绘制长度和不绘制长度
* @param count 数组长度,当数组为NULL时,为0
*/
CGContextSetLineDash(ctx, 10.0, lengths, 2);
IMG_0253.PNG
再引出内容
真正闭合的子路径(调用了CGContextClosePath)和通过添加一条连接起点的线,这两种方式之间的区别
- 闭合路径将起点看作一个连接点,按照选择的line-join进行渲染
IMG_0254.PNG
- 没有真正闭合路径两个端点按照选择的line-cap进行渲染
IMG_0255.PNG
A closed subpath treats the starting point as a junction between connected line segments; the starting point is rendered using the selected line-join method. In contrast, if you close the path by adding a line segment that connects to the starting point, both ends of the path are drawn using the selected line-cap method.
绘制路径
大致可以分为两类 :
- 以描边的方式绘制路径 : kCGPathStroke、kCGPathFillStroke、kCGPathEOFillStroke
- 以填充的方式绘制路径 : kCGPathFill、kCGPathEOFill、kCGPathFillStroke、kCGPathEOFillStroke
描边和填充不冲突,其中值得一说的是填充
根据一个像素点是否应该被绘制,可以分为两类规则 :
首先,从这个像素点开始,向外画一条线
- nonzero winding number rule : 非零卷绕数规则
穿过这条线的路径,左 -> 右,计数+1,右 -> 左,计数-1。最后,如果计数为0,说明点在路径之外,如果计数不为0,说明点在路径之内,那么绘制该点
关于卷绕数,在本文最后的glossary - even-odd rule : 偶数-奇数规则
穿过这条线的路径,如果次数是偶数,说明点在路径之外,如果次数是奇数,说明点在路径之内,那么绘制该点
9BFB146C-0D4A-47C3-966F-849F77386882.png
裁剪区域
裁剪区域就是充当遮罩作用,当绘制时,Quartz只会渲染裁剪区域的内部。裁剪区域也是图形状态的一部分
混合模式
混合模式决定前景和背景混合的效果。混合模式也是图形状态的一部分,默认为kCGBlendModeNormal
kCGBlendModeNormal和kCGBlendModeColorBurn的对比
IMG_0257.PNG
IMG_0256.PNG
Graphics States
图形状态单独拿出来说因为这货太重要了。当我们路径创建完成,开始绘制路径时,当前图形状态(current graphics state)决定如何渲染。例如上面提到,线宽、端点风格、连接点风格、虚线模式、描边颜色、填充颜色、裁剪区域、CTM等等,这些全部保存在当前图形状态中
图形上下文包含一个图形状态栈。当Quartz创建图形上下文时,栈是空的。当为了保存当前绘图状态时,可以调用CGContextSaveGState,这个操作相当于将当前图形状态拷贝一份入栈,当为了恢复之前绘图状态时,可以调用CGContextRestoreGState,这个操作相当于将栈顶图形状态出栈,刚出栈的图形状态成为当前图形状态
值得注意的是 : 当前路径不是图形状态的一部分
076035AE-44DC-43DE-A2B0-4FA24B5F9D4C.png
Color and Color Spaces
-
CGContextSetAlpha
设置图形上下文全局alpha,如果即设置了颜色的最后一个参数alpha,又通过CGContextSetAlpha设置了全局alpha,最终颜色是两者相乘
CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetAlpha(ctx, 0.5); [[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5] setFill]; CGContextAddRect(ctx, CGRectMake(center.x - 150.0, center.y - 150.0, 300.0, 300.0)); CGContextFillPath(ctx);
IMG_0258.PNG
You can supply an alpha value as the last color component to all routines that accept colors. You can also set the global alpha value using the CGContextSetAlpha function. Keep in mind that if you set both, Quartz multiplies the alpha color component by the global alpha value.
opaque和alpha的关系
在默认混合模式下,也就是说,CGContextSetBlendMode等于kCGBlendModeNormal,Quartz执行下面的计算公式,混合原始颜色和目标颜色 :
当opaque为NO时,alpha不为1.0,destination = (alpha \ source) + (1 - alpha) \ destination
当opaque为YES时,alpha为1.0,上面的计算公式 => destination = source
source : 原始颜色,destination : 目标颜色,alpha : 原始颜色的alpha和全局的alpha的合成
显然,当opaque为YES,也就是完全不透明时,显示原始颜色,alpha通道被忽略并且优化位图储备
当opaque为NO,也就是完全透明或者部分透明时,原始颜色和目标颜色混合,位图必须包括alpha通道去处理部分透明像素
Transforms
当前变换矩阵(current transformation matrix)
Quartz定义了两种完全分离的坐标 : 用户空间以及设备空间。通过操作当前变化矩阵(简称CTM),修改用户空间。CTM总是代表用户空间和设备空间之间当前映射关系
Identity
IMG_0259.PNG
-
CGContextTranslateCTM
平移用户空间坐标系原点
CGContextTranslateCTM(ctx, 100.0, 50.0);
IMG_0263.PNG
-
CGContextScaleCTM
缩放用户空间坐标系
CGContextScaleCTM(ctx, 0.5, 0.5);
IMG_0264.PNG
-
CGContextRotateCTM
旋转用户空间坐标
CGContextRotateCTM(ctx, AngleToRadian(30));
IMG_0265.PNG
先平移,再缩放,最后旋转
CGContextTranslateCTM(ctx, 50.0, 50.0); CGContextScaleCTM(ctx, 0.5, 0.5); CGContextRotateCTM(ctx, AngleToRadian(30));
这样的连续操作CTM也可以使用CGContextConcatCTM
-
CGContextConcatCTM
连接CTM和仿射变换(矩阵与矩阵相乘),
CGContextConcatCTM(ctx, CGAffineTransformMakeTranslation(50.0, 50.0)); CGContextConcatCTM(ctx, CGAffineTransformMakeScale(0.5, 0.5)); CGContextConcatCTM(ctx, CGAffineTransformMakeRotation(AngleToRadian(30)));
IMG_0266.PNG
仿射变换
仿射变换函数操作的是矩阵,而不是CTM。先通过仿射变换函数构造矩阵,再通过CGContextConcatCTM应用于CTM
- CGAffineTransformMakeTranslation
通过平移值直接构造一个仿射变换 - CGAffineTransformTranslate
通过平移一个已经存在的仿射变换构造一个新的仿射变换
CGContextConcatCTM(ctx, CGAffineTransformTranslate(CGAffineTransformIdentity, 50.0, 50.0)); CGContextConcatCTM(ctx, CGAffineTransformMakeTranslation(50.0, 50.0));
- CGAffineTransformMakeScale
通过缩放值直接构造一个仿射变换 - CGAffineTransformScale
通过缩放一个已经存在的仿射变换构造一个新的仿射变换
CGContextConcatCTM(ctx, CGAffineTransformScale(CGAffineTransformIdentity, 0.5, 0.5)); CGContextConcatCTM(ctx, CGAffineTransformMakeScale(0.5, 0.5));
- CGAffineTransformMakeRotation
通过旋转值直接构造一个仿射变换 - CGAffineTransformRotate
通过旋转一个已经存在的仿射变换构造一个新的仿射变换
CGContextConcatCTM(ctx, CGAffineTransformRotate(CGAffineTransformIdentity, AngleToRadian(30))); CGContextConcatCTM(ctx, CGAffineTransformMakeRotation(AngleToRadian(30)));
对于之前连续操作CTM也可以先获得最终仿射变换,再通过CGContextConcatCTM应用于CTM
CGAffineTransform translation = CGAffineTransformTranslate(CGAffineTransformIdentity, 50.0, 50.0); CGAffineTransform scale = CGAffineTransformScale(translation, 0.5, 0.5); CGAffineTransform rotation = CGAffineTransformRotate(scale, AngleToRadian(30)); CGContextConcatCTM(ctx, rotation);
CGAffineTransform
我们知道,对于一个线性变换,可以使用矩阵表示。Quartz使用下面的矩阵表示从点(x,y)变换到点(x',y')的线性变换
A4F46E1E-E94D-4988-85D4-EF5E69227671.png
对应的线性方程
A9DF683F-826F-4FF4-A1F4-8252931A8F27.png
矩阵只有当第一个矩阵的列数等于第二个矩阵的行数时才有意义 矩阵的最右边一列就是这么来的
-
CGAffineTransformMake
就是用来构造这样一个矩阵的结构体,由于右边一列没有意义,它控制的参数只有6个
@param a x坐标的系数,控制缩放
@param b 控制平行于y轴的切变
@param c 控制平行于x轴的切变
@param d y坐标的系数,控制缩放
@param tx x坐标的常量,控制平移
@param ty y坐标的常量,控制平移
abcd一起控制旋转
struct CGAffineTransform {
CGFloat a, b, c, d;
CGFloat tx, ty;
};
Identity
|1 0 0| |x' y' 1| = |x y 1||0 1 0| |0 0 1|
x' = x y' = y
平移
|1 0 0| |x' y' 1| = |x y 1||0 1 0| |tx ty 1|
x' = x + tx y' = y + ty
缩放
|sx 0 0| |x' y' 1| = |x y 1||0 sy 0| |0 0 1|
x' = x * sx y' = y * sy
切变
|1 sy 0| |x' y' 1| = |x y 1||sx 1 0| |0 0 1|
x' = x + y * sx y' = x * sy + y
旋转
|cosα sinα 0| |x' y' 1| = |x y 1||-sinα cosα 0| |0 0 1|
x' = x * cosα - y * sinα y' = x * sinα + y * cosα
我们试着验证一下旋转
原始坐标有一点p(x,y),现在假设坐标系逆时针旋转了α角度,那么点p相对新坐标系变为p(x',y')
9383512B-4820-41B7-839A-C8F126DE2ABB.png
过点p作垂直于y轴的垂线,交旧坐标系于点a,交新坐标系于点c,过点a继续作垂线,交垂线于点d,交新坐标系于点b。
显然,∠apc等于α,四边形abcd是个矩形,cp = dp - dc,那么cp = dp - ab。dp =ap * cosα,ab = oa * sinα,cp就是x',ap就是x,oa就是y,从而验证 x' = x * cosα - y * sinα
,同样方式,可以验证 y' = x * sinα + y * cosα
-
CGAffineTransformEqualToTransform
判断两个仿射变换是否相等
CGAffineTransformEqualToTransform(CGAffineTransformMake(1.0, 0.0, 0.0, 1.0, 0.0, 0.0), CGAffineTransformIdentity)
-
CGAffineTransformInvert
翻转一个仿射变换
关于模式,阴影,渐变,透明图层等等暂时没有涉及