Core Graphics 学习 (一) 坐标系,Path,简单路径绘制

Core Graphics 也叫 Quartz 2D, 是 iOS,TVOS,macOS应用开发中先进的二维绘制引擎.

在iOS中, QuartZ 2D 可与所有可用的图形和动画技术 (如 Core Animation, OpenGL和UIKit类)配合使用.

Quartz 2D使用 painters model进行成像. 一个对象绘制之后只用通过不对新的绘制操作来进行修改.不同元素绘制顺序不同,展示的效果可能不同.

使用Core Graphics 可以做到以下事情:
图层透明度,阴影
根据路径(path)绘图
离屏渲染
色彩管理
抗锯齿渲染
PDF文件管理(创建,展示,解析)

QuartZ 2D坐标系

QuartZ 2D默认坐标系如下. 左下角为原点(0,0),横向像右为x正向,列向向上为y正向.


image.png

因为不同的设施具有不同的底层呈像能力,图形的位置和大小必须以独立于设施的方式来进行定义.例如,一些屏幕设施一英寸可能显示不超过96像素,但是打印设置打印一英寸显示300个像素.如果在某一设施上定义坐标系,那么在其他设施上就不能正常显示需要绘制的图像,会出现失真.

QuartZ 2D 独立的坐标系统 - 用户空间 (user space), 经过当前矩阵变换(current transformation matrix,或 CTM),将图像映射到设施坐标系统 - 设施空间(divece space). CTM是进行仿射变换的特定类型的矩阵,可以通过平移,旋转,缩放等操作,将一个点从一个坐标系映射到另一个坐标系.

在一些绘图技术中使用额坐标系和QuartZ 2D的默认坐标系不同. 它的源点在左上角,并且y轴向下是正方向.例如下面几种情况使用的就是这种坐标系:
在Mac OS X中,子类化一个NSView, 重写isFlipped方法,返回YES,获得的坐标系.
在iOS中, UIView返回的绘图上下文使用的坐标系.
在iOS中, 通过UIGraphicsBeginImageContextWithOptions方法创建绘图的上下文.

UIKit返回的绘图上下文使用修改过的坐标系,是因为UIKit本身使用的坐标系就是不同于默认坐标系的; UIKit会自动将它创建的绘图上下文的坐标系转换为于它本身的坐标系相匹配.如果一个应用程序想要使用相同的规则创建UIView 和 PDF上下文(由Quartz 和它的默认坐标系绘制) , 则需要对PDF的坐标系进行转换,也就是将原点转化到左上角,将y值变成-y. 下图展示坐标转换之后的效果,默认坐标系上是顺时针,在转化之后的坐标系上是逆时针:


image.png

图形上下文 (Graphics Context)

图像上下文 (the Graphics Context), 用于封装QuartZ 用于将图像绘制到输出设施(如PDF文件,位图或窗口) 上的信息.图像上下文中的信息包括图像绘图参数和特定设施中的页面绘制内容. QuartZ中的所有对象都被绘制到或包含在图形上下文中.

在IOS中绘制视图上下文
要在IOS 应用程序中绘制屏幕, 可以设置UIView对象并实现其drawRect: 方法来执行绘图. 在调用自定义drawRect: 方法之前,视图对象会自动配置其绘图环境,以便代码可以立即开始绘制.
let contextRef = UIGraphicsGetCurrentContext()

创建PDF图像上下文

当你创建一个PDF绘图上下文,并在上面进行绘制, 那么QuartZ将会将你的绘制当作一系列的PDF绘制命令,写入到一个文件.通过给定一个PDF输出位置和一个默认的媒体框(meida box) - 也就是一个特定范围的页面.

QuartZ 2D API 提供了两个创建PDF图形上下文的方法:
方法一:
func creatPDFContext(with filePath: CFString, mediaBox: inout CGRect) -> CGContext? {
// 根据文件的路径创建 url let url = CFURLCreateWithFileSystemPath(nil, filePath, .cfurlposixPathStyle, false) guard (url != nil) else { return nil } // 根据 url 和 medioBox 创建 pefCOntext let pdfContect = CGContext.init(url!, mediaBox: &mediaBox, nil) return pdfContect}

方法二:

func creatPDFContext2(with filePath: CFString, mediaBox: inout CGRect) -> CGContext? { let url = CFURLCreateWithFileSystemPath(nil, filePath, .cfurlposixPathStyle, false) guard (url != nil) else { return nil } let dataConsumer = CGDataConsumer.init(url: url!) guard dataConsumer != nil else { return nil } let pdfContext = CGContext.init(consumer: dataConsumer!, mediaBox: &mediaBox, nil) return pdfContext }

IOS 中 PDF图形上下文使用的是 QuartZ提供的默认坐标系

创建位图图形上下文(Bitmap Graphics Context)
位图上下文接受一个指向内存缓冲区的指针,它包含了位图的存储空间.当你在位图上下文中进行绘制时,缓存区会进行更新. 在释放了位图上下文之后,将会完全按照你指定的像素更新位图.

在iOS应用程序中应该使用UIGraphicsBeginImageContextWithOptions方式创建位图上下文.因为使用这种方式创建的位图上下文,UIKit会将相同的放射变换应用于上下文,就想UIView创建的上下文.

抗锯齿效果(Aniti-Aliasing)
位图上下文支持抗锯齿功能.通过设置 setAllowsAntialiasing为 true,允许抗锯齿.

Paths

path 定义了一个或多个形状; 或定义多条自路径. 一条子路径可能包含直线和曲线.
创建,绘制路径:
路径的创建,绘制是独立的任务.首先,创建一条路径; 当需要渲染一条路径的时候,再去绘制它.可以选择对路径进行描边(stroke), 填充(fill), 剪裁(clip).

绘制构建模块

子路径由直线(Lines), 弧线(Arcs),曲线(Curves) 构成.

Point
QuartZ的机制是持续追踪当前点. 当使用 move(to point: CGPoint)方法设置一个点(10,10),这个值就是当前点.
// 指定一个子路径的开始点 public fuc move(to point: CGPoint)

Line
一条直线的位置取决于终点的位置.
知道当前点,并给定一个终点,创建一条直线路径.
// 指定一个子路径的开始点 : public func move(to point: CGPoint)
// 创建一条直线路径 public func addLine(to Point: CGPoint)

还可以之赐你个给定一连串点包括当前点和每条直线的终点,创建相连的多条直线:
public func addLines(between points:[CGPoint])

Arcs

Arcs是圆形的分段. 提供了两个方法进行创建.

根据圆心,半径,角度绘制(clockwise 为true 为顺时针):
public func AddArc(center: CGPoint,radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)

当前点第一个end点的连线,第一个end点与第二个end点的连线构成相交的两个线段,绘制与这两条直线相切并满足半径条件的圆弧.
public fuc addArc(tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat)

Curves
曲线主要是贝塞尔曲线: 三阶贝塞尔曲线,二阶贝塞尔曲线.

三阶贝塞尔曲线
public func addCurve(to end: CCGPoint,control1: CGPoint, control2: CGPoint)
二阶贝塞尔曲线
public fun addQuadCurve(to end; CGPoint, control: CGpoint)

closePath
关闭并终止当前路径的子路径.从当前点追加一行到当前子路径的起点,并结束子路径.关闭子路径后,您的应用程序可以开始一个新的子路径,而无需首先调用moveTo(x: y:). 在这种情况下,新的子路径被隐式创建,起始点和当前点等于先前子路径的起始点.如果当前路径为空或当前子路径已经关闭,则此功能不起作用.
public func closePath()

*** 椭圆(Elipses)***
椭圆是压扁的圆. QuartZ可以在给定的矩形区域内绘制相应的椭圆,如果长宽相等,绘制的是圆.
public func addElipse(in rect: CGRect)

矩形(Rectangles)

绘制一个矩形
public func addRect(_rect: CGRect)
绘制多条矩形
public func addRects(_rects:[CGRect])

创一条路径的步骤

  1. 开始路径
    contextRef!.beginPath()
  2. 为第一个形状,子路径设置开始点
    contextRef!.move(to: CGPont(x:10, y:10))
  3. 之后根据需求绘制相应的路径(直线,曲线,矩形).....

注意以下几点:
beginPath()
起始点:move(to: CGPoint(x: 10, y: 10))
关闭路径:closePath()
当您绘制弧线时,Quartz会在当前点和弧线起点之间画一条线。
创建矩形、椭圆的时候,相当于添加了一个新的封闭子路径
必须调用绘图函数来填充或描绘路径,因为创建路径不会绘制路径。

绘制路径之后,路径从图像上下文中刷新.你可能想保存这个路径,特别是如果它描绘了一个复杂的场景,你不想要一遍又一遍的创建路径.因此,QuartZ提供了两种创建可重用路径的数据类型-CGPath和CGMutablePath. 可以调用函数CGPathCreteMutable创建一个可变CGPath对象, 您可以向其中添加线,弧,曲线和矩形. QuartZ提供了一组CGPath函数:

let mutablePath = CGMutablPath.init()
相当contextRef!.beginPath()
mutablePath.move(to: CGPoint(x:10 , y:10))
相当于 contextRef!.move(to: CGPoint(x: 10, y:10))
mutablePath.addLine(to:CGPoint(x:60, y:10))
添加直线
mutablePath.addRect(CGRect(x: 0, y: 0, width: 50, height: 60))
添加矩形
mutablePath.addEllipse(in: CGRect(x: 0, y: 0, width: 50, height: 30))
添加椭圆
mutablePath.addArc(tangent1End: CGPoint(x: 60, y: 10), tangent2End: CGPoint(x: 10, y: 60), radius: 50)
添加圆弧
mutablePath.addCurve(to: CGPoint(x: 60,y: 10), control1: CGPoint(x: 20, y: 0), control2: CGPoint(x: 40, y: 20))
添加贝塞尔曲线
mutablePath.closeSubpath()
封闭路径
contextRef!.addPath(mutablePath)
添加路径到图形上下文

绘制路径

当创建好路径之后,接下来就是要将路径绘制到上下文. 绘制路径有两种方式: Stroking(描边). filling(填充)

Stroking(描边)

关于stroking的常用属性参数:

contextRef!.setLineWidth(1.0) // 线宽
contextRef!.setLineJoin(.round) // 设置连接点样式
contextRef!.setLineCap(.round) // 设置端点样式
contextRef!.setMiterLimit(10.0) //如果连线设置为斜角(miter), 用来判断连接点是miter还是bevel. 斜角的长度除以线宽.如果结果大于miter限制,则将样式转换为 bevel
contextRef!.setLineDash(phases: 0 ,lengths:[2,3,5]) // phase: 表示绘制虚线的位置, lengths: 表示实线,虚线之间的间隔
contextRef!.setStrokeColor(UIColor.red.cgColor) // 设置描边颜色

Stroking 常用函数:

contextRef!.strokePath() // 为当前路径描边
contextRef!.stroke(rect:rect) // 描边特定的矩形
contextRef!.stroke(rect: rect, width: 1.0) // 根据特定的线宽,描边一个矩形
contextRef!.strokeEllipse(in: rect) // 描边一个椭圆
contextRef!.strokeLineSegments(between: [point1, point2 point3, point4]) // 描绘一系列相连线段。
contextRef!.drawPath(using: .stroke) // 根据提供的绘制模式,绘制

连接点样式:

image.png

线端点样式:

image.png

线破折号样式:

image.png

填充路径(Filling a Path)
当一个路径是封闭路径,可以进行填充.填充之前需要先计算填充区域. QuartZ可以通过两种方式计算天长区域.诸如圆形和矩形的简单路径具有明确的区域. 但是,如果您的路径由重叠段组成,或者如果路径包含多个子路径,例如同心圆,则可以使用这两个规则来确定填充区域.

默认的填充规则是 非零绕线数规则. 决定一个点是否需要绘制,从该点开始绘制一条超出图形边界的射线. 从零开始计数, 如果路径线段从左向右穿过该射线,计数加一; 相反减一; 如果结果是零就不绘制该点.路径的绘制方向会影响填充效果.

还可以选择 奇偶规则。 还是先从一点绘制一条射线穿过路径边界,从零记录穿过的路径边界的次数,奇数就绘制该点,偶数就不绘制。

image.png

填充路径的方法:

contextRef!.fillPath(using: .evenOdd) // 使用 even-odd 规则填充
contextRef!.fillPath(using: .winding) // 使用 nonzero winding number 规则填充
contextRef!.fill(CGRect(x: 0, y: 0, width: 50, height: 50)) // 填充一个矩形
contextRef!.fill([CGRect(x: 0, y: 0, width: 50, height: 50)]) // 填充多个矩形
contextRef!.fillEllipse(in: CGRect(x: 0, y: 0, width: 50, height: 50)) // 填充一个椭圆
contextRef!.drawPath(using: .fill) //根据提供的绘制模式,绘制

设置混合模式(Blend Modes)

混合模式指定QuartZ怎样在背景上使用涂料. QuartZ默认使用普通混合模式,它使用以下公式将前景绘图与背景绘画相结合:
result = (alpha foreground) + (1 - alpha) background

对于下面的示例,您可以假定颜色完全不透明(alpha值= 1.0)。对于不透明的颜色,当使用普通混合模式进行绘制时,任何在背景之上绘制的内容完全遮挡背景上的绘图。

您可以通过调用函数CGContextSetBlendMode来设置混合模式以实现各种效果,传递适当的混合模式常量。请记住,混合模式是图形状态的一部分。如果在更改混合模式之前使用函数CGContextSaveGState,则调用函数CGContextRestoreGState会将混合模式重置为 normal。

contextRef!.setBlendMode(.color)

剪切路径(Clipping
当前的裁剪区域通过一个路径创建,用来充当 mask 的作用,允许阻止显示不想绘制的页面部分。例如,如果有一个非常大的位图图像,并且仅需要显示其一小部分,则可以将剪切区域设置为仅要显示的部分。

裁剪方法:

contextRef!.clip() // 使用 nonzero winding number rule 来计算裁剪路径
contextRef!.clip(using: .evenOdd) // 使用 even-odd rule
contextRef!.clip(to: CGRect(x: 0, y: 0, width: 50, height: 50))contextRef!.clip(to: CGRect(x: 0, y: 0, width: 50, height: 50), mask: nil)
// 创建一个圆形裁剪区域

根据上述知识绘制一些基本图形

//: MARK: 绘制直线 func drawLine(_ context: CGContext, rect: CGRect) { // 描边的属性 context.setStrokeColor(red: 0, green: 0, blue: 1, alpha: 1) context.setLineWidth(2.0) context.setLineCap(.round) // 绘制一条直线 context.move(to: CGPoint(x: 10, y: 10)) context.addLine(to: CGPoint(x: rect.size.width - 20, y: 10)) context.strokePath() } //: MARK: 绘制多条直线,组成一个三角形 func drawLines(_ context: CGContext, rect: CGRect) { // 设置属性 context.setStrokeColor(red: 1, green: 0, blue: 0, alpha: 1) context.setLineWidth(3.0) context.setLineJoin(.round) context.setLineCap(.round) // 绘制 let startPoint = CGPoint(x: 60, y: 20) let point2 = CGPoint(x: 110, y: 120) let point3 = CGPoint(x: 10, y: 120) context.addLines(between: [startPoint, point2, point3]) context.closePath() context.strokePath() } //: MARK: 绘制一条圆弧 func drawArcs(_ context: CGContext, rect: CGRect) { // 设置属性 context.setStrokeColor(red: 1, green: 1, blue: 0, alpha: 1) context.setLineWidth(3.0) context.setLineCap(.round) // 绘制 // 方式一:当前点于第一个end点的连线,第一个end点与第二个end点的连线构成相交的两个线段,绘制与这两条直线相切并且满足半径条件的圆弧。 context.move(to: CGPoint(x: 120, y: 20)) context.addArc(tangent1End: CGPoint(x: 220, y: 20), tangent2End: CGPoint(x: 120, y: 120), radius: 20) context.strokePath() // 方式二: // clockwise 为 true 是顺时针, false 是 逆时针 context.addArc(center: CGPoint(x: 300, y: 70), radius: 50, startAngle: CGFloat(-Double.pi / 2), endAngle: CGFloat(Double.pi / 3), clockwise: false) context.strokePath() } //: MARK: 绘制贝塞尔曲线 func drawCurves(_ context: CGContext, rect: CGRect) { // 设置属性 context.setStrokeColor(red: 1, green: 0.5, blue: 0.3, alpha: 1) context.setLineWidth(3.0) context.setLineCap(.round) // 绘制 // 二次 context.move(to: CGPoint(x: 10, y: 140)) context.addQuadCurve(to: CGPoint(x: rect.size.width - 10, y: 140), control: CGPoint(x: 50, y: 200)) context.strokePath() // 三次 context.move(to: CGPoint(x: 10, y: 250)) context.addCurve(to: CGPoint(x: rect.size.width - 10, y: 250), control1: CGPoint(x: 200, y: 210), control2: CGPoint(x: rect.size.width - 200, y: 290)) context.strokePath() } //: MARK: 绘制椭圆 func drawElipses(_ context: CGContext, rect: CGRect) { // 设置属性 context.setFillColor(red: 0.3, green: 0.6, blue: 0.9, alpha: 1) context.setStrokeColor(red: 0.7, green: 0.5, blue: 0.3, alpha: 1) context.setLineWidth(3.0) context.setLineCap(.round) // 绘制 context.addEllipse(in: CGRect(x: 10, y: 270, width: rect.size.width - 20, height: 100)) context.drawPath(using: .fillStroke) } //: MARK: 绘制矩形 func drawRect(_ context: CGContext, rect: CGRect) { // 设置属性 context.setFillColor(red: 0.3, green: 0.6, blue: 0.9, alpha: 1) context.setStrokeColor(red: 0.7, green: 0.5, blue: 0.3, alpha: 1) context.setLineWidth(3.0) context.setLineCap(.round) // 绘制 context.addRect(CGRect(x: 10, y: 380, width: rect.size.width - 20, height: 100)) context.drawPath(using: .fillStroke) } //: MARK: 绘制一个同心圆 func drawConcentricCicles(_ context: CGContext, rect: CGRect) { // 设置属性 context.setFillColor(red: 0.3, green: 0.6, blue: 0.9, alpha: 1) context.setStrokeColor(red: 0.7, green: 0.5, blue: 0.3, alpha: 1) context.setLineWidth(3.0) context.setLineCap(.round) // 绘制 context.addArc(center: CGPoint(x: rect.size.width / 2, y: rect.size.height - 80), radius: 80, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi2), clockwise: false) context.addArc(center: CGPoint(x: rect.size.width / 2, y: rect.size.height - 80), radius: 40, startAngle: CGFloat(Double.pi2), endAngle: CGFloat(0), clockwise: true) context.fillPath(using: .evenOdd) }

绘制结果:

image.png

Demo:
https://github.com/luckySlider/CoreGraphics.git

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容