爱上你!Quartz 2D

转载自: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.

绘制路径

大致可以分为两类 :

  1. 以描边的方式绘制路径 : kCGPathStroke、kCGPathFillStroke、kCGPathEOFillStroke
  2. 以填充的方式绘制路径 : kCGPathFill、kCGPathEOFill、kCGPathFillStroke、kCGPathEOFillStroke

描边和填充不冲突,其中值得一说的是填充

根据一个像素点是否应该被绘制,可以分为两类规则 :

首先,从这个像素点开始,向外画一条线

  1. nonzero winding number rule : 非零卷绕数规则
    穿过这条线的路径,左 -> 右,计数+1,右 -> 左,计数-1。最后,如果计数为0,说明点在路径之外,如果计数不为0,说明点在路径之内,那么绘制该点
    关于卷绕数,在本文最后的glossary
  2. 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

    翻转一个仿射变换

关于模式,阴影,渐变,透明图层等等暂时没有涉及

Glossary


1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值