×

Quartz2D --> 二维绘图引擎(二 - 路径、颜色、形变)

96
寻形觅影
2017.07.26 17:10* 字数 3558

一、Paths (路径)

路径定义了一个或多个形状或子路径。子路径可以由直线、曲线或两者兼而有之,它可以是打开的或闭合的。子路径可以是一个简单的形状,比如一条直线,圆,矩形,星型或更复杂的形状,如山脉的轮廓或抽象涂鸦等等。这一部分主要介绍和了解构建路径的模块,如何画和描绘路径,影响路径外观的参数等。

1、The Building Blocks

子路径是由线、弧和曲线构成,同时Quartz也提供了通过一个函数调用添加矩形和椭圆的方法。点也是路径的基本构建块,因为点定义了图形的开始和结束位置。

  • Point(点):点是在用户空间中指定一个位置,可以调用这个函数CG_EXTERN void CGContextMoveToPoint(CGContextRef cg_nullable c, CGFloat x, CGFloat y)指定一个新的子路径的起始位置。Quartz会持续跟踪当前点(当前点-- current point 是绘图过程中重要的参考,代表路径最后的位置)。例如:如果调用该函数CGContextMoveToPoint() 设置一个位置(10,10),如果这时再画一条水平线50单位长,那么最后一点就是(60,10),这一点也就成为了成为当前点。线、弧和曲线总是从当前点开始描画的。
  • Lines (线 ):线的开始必须要有当前点!当当前点存在那么决定线的就只有结束点了。Quartz中使用函数CG_EXTERN void CGContextAddLineToPoint(CGContextRef cg_nullable c, CGFloat x, CGFloat y) 去定义一条线,还可以使用函数CG_EXTERN void CGContextAddLines(CGContextRef cg_nullable c, const CGPoint * __nullable points, size_t count)去设置一组线。
  • Arcs(弧):Quartz提供了两个函数创建弧:1、CG_EXTERN void CGContextAddArc(CGContextRef cg_nullable c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise)该函数参数包括图形上下文、圆心、半径、起始弧度、终止弧度和是否顺时针。2、CG_EXTERN void CGContextAddArcToPoint(CGContextRef cg_nullable c, CGFloat x1, CGFloat y1, CGFloat x2, CGFloat y2, CGFloat radius)该函数是定义了圆弧的两个端点和半径,如下图所示方法创建圆弧。
红色部分为创建的圆弧
  • Curves(曲线):这里主要是指二次贝塞尔曲线和三次贝塞尔曲线。构建二次贝塞尔曲线函数:CG_EXTERN void CGContextAddQuadCurveToPoint(CGContextRef cg_nullable c, CGFloat cpx, CGFloat cpy, CGFloat x, CGFloat y),该函数包含一个控制点(cpx, cpy)和一个终点(x, y)。构建三次贝塞尔曲线函数:CG_EXTERN void CGContextAddCurveToPoint(CGContextRef cg_nullable c, CGFloat cp1x, CGFloat cp1y, CGFloat cp2x, CGFloat cp2y, CGFloat x, CGFloat y)该函数包含两个控制点和一个终点。
    二次贝塞尔曲线
三次贝塞尔曲线
  • Closing a Subpath(关闭一个子路径):CG_EXTERN void CGContextClosePath(CGContextRef cg_nullable c).需要注意的是当你关闭一个子路径开始一个新的路径时,该路径是从刚才关闭子路径的当前点开始的。
  • Ellipses(椭圆):Quartz使用这一函数来定义椭圆:CG_EXTERN void CGContextAddEllipseInRect(CGContextRef cg_nullable c, CGRect rect) 。根据提供的rect去描绘椭圆,椭圆为rect组成矩形的内切椭圆。如果rect组成的矩形是正方形则是圆。
  • Rectangles(矩形):Quartz使用这一函数来定义矩形:CG_EXTERN void CGContextAddRect(CGContextRef cg_nullable c, CGRect rect),同时使用CG_EXTERN void CGContextAddRects(CGContextRef cg_nullable c, const CGRect * __nullable rects, size_t count)这一函数定义了一组矩形。

2、Creating a Path

当你想在上下文中构建一条path时需要通过调用CG_EXTERN void CGContextBeginPath(CGContextRef cg_nullable c)函数,然后设置起点(当前点)或者通过调用函数CG_EXTERN void CGContextMoveToPoint(CGContextRef cg_nullable c, CGFloat x, CGFloat y)在path中设置子路径,当你建立起第一个点后你就可以进行其他操作了。

当你画一条路径后,需要从图形上下文中刷新时。你可能不希望失去你的路径,尤其是它描绘了你想要多次使用的一个复杂的场景。出于这个原因,Quartz提供了两种数据类型来创建可重用的paths : CGPathRefCGMutablePathRef。你可以调用函数CG_EXTERN CGMutablePathRef CGPathCreateMutable(void)去创建一个可变的CGPath对象然后你就可以添加线、弧、曲线等操作。需要注意的是路径功能操作的是CGPath对象,而不是图形上下文。

下面列举一些CGPath替代CGContext的一些方法:

CGPathCreateMutable, which replacesCGContextBeginPath
CGPathMoveToPoint, which replaces CGContextMoveToPoint
CGPathAddLineToPoint, which replaces CGContextAddLineToPoint
CGPathAddCurveToPoint, which replaces CGContextAddCurveToPoint
CGPathAddEllipseInRect, which replaces CGContextAddEllipseInRect
CGPathAddArc, which replaces CGContextAddArc
CGPathAddRect, which replaces CGContextAddRect
CGPathCloseSubpath, which replaces CGContextClosePath

当你想要为图形上下文添加路径时,可以调用CG_EXTERN void CGContextAddPath(CGContextRef cg_nullable c, CGPathRef cg_nullable path)方法,这样直到你去绘制该上下文前,这一路径都会存在于上下文中。

这里只是一些常用方法,详细可以参考 CoreGraphics框架下的CGContext.h文件和CGPath.h文件!

3、Painting a Path

绘制路径可以通过描线或者填充或者两者兼有的方式,Stroking:仅仅是描绘路径成线,Filling:会填充路径所包含的区域。

影响Stroking绘制的参数:

参数 设置参数值的函数
线宽 CGContextSetLineWidth
线帽类型 CGContextSetLineCap
线段间连接样式 CGContextSetLineJoin
斜接限制 CGContextSetMiterLimit
行缓冲模式 CGContextSetLineDash
绘线颜色空间(stroke) CGContextSetStrokeColorSpace
绘线颜色 CGContextSetStrokeColor、CGContextSetStrokeColorWithColor
纹理 CGContextSetStrokePattern

PS:关于线帽类型、线段间连接样式、斜接限制、行缓冲模式可以参考这篇文章中相关内容

绘制路径的函数:

函数 功能
CGContextStrokePath 绘制路线
CGContextStrokeRect 使用当前线宽和线色绘制矩形
CGContextStrokeRectWithWidth 使用指定线宽绘制矩形
CGContextStrokeEllipseInRect 绘制椭圆
CGContextStrokeLineSegments 绘制线段
CGContextDrawPath 指定模式下渲染路径

PS:绘制路径模型

typedef CF_ENUM (int32_t, CGPathDrawingMode) {
  kCGPathFill,  //只有填充(非零缠绕数填充),不绘制边框
  kCGPathEOFill, //奇偶规则填充
  kCGPathStroke, //只有边框
  kCGPathFillStroke, // 既有边框又有填充
  kCGPathEOFillStroke //奇偶填充并绘制边框
};

设置混合模式:
  使用CG_EXTERN void CGContextSetBlendMode(CGContextRef cg_nullable c, CGBlendMode mode)函数设置混合模式,混合模式是图形状态的一部分,如果在改变混合模式之前使用CG_EXTERN void CGContextSaveGState(CGContextRef cg_nullable c)函数保存了状态,那么需要在完成后使用CG_EXTERN void CGContextRestoreGState(CGContextRef cg_nullable c)函数重置混合模式为默认正常情况。关于混合模式枚举参考该文章最后部分

4、Clipping to a Path

当前剪切区域是一个作为遮罩的路径为基础创建的,允许屏蔽部分你不想绘制展示的页面。常见用于图片处理。
当你绘制时,Quartz只会渲染剪切的区域。同样剪切区域也是图形状态的一部分,为了使剪裁区域恢复到以前的状态,可以在剪辑之前保存图形状态,当完成后,再恢复图形状态即可。
设置剪切函数:

函数 说明
CGContextClip 结果路径即当前剪切路径与上下文路径的交集,使用结果路径作为后续渲染操作的剪切路径。这里遵循非零环绕规则。
CGContextEOClip 使用奇偶规则渲染,除此外与CGContextClip相同
CGContextClipToMask 为上下文剪切区域添加一个转换到指定rect的mask(CGImageRef类型),根据提供的mask是image 还是 image mask不同决定输出的结果不同。
CGContextClipToRect 取当前上下文与rect的交集,注意:这个函数会将上下文的路径重置为空路径
CGContextClipToRects 取当前上下文与由rects中所有元素组成的剪辑区域的交集,同样,这个函数会将上下文的路径重置为空路径
CGContextGetClipBoundingBox 该函数有一个CGRect类型的返回值,The bounding box 是包含剪辑区域内所有点的最小矩形

路径设置举例:

- (void)drawRect:(CGRect)rect {
    
    CGContextRef currentContextRef = UIGraphicsGetCurrentContext();
    CGMutablePathRef mutablePath = CGPathCreateMutable();
    CGPathMoveToPoint(mutablePath, NULL, 0, 0);
    CGPathAddLineToPoint(mutablePath, NULL, self.frame.size.width, 0);
    CGPathAddQuadCurveToPoint(mutablePath, NULL, self.frame.size.width/2, self.frame.size.height/2, self.frame.size.width, self.frame.size.height);
    CGPathCloseSubpath(mutablePath);
    CGContextSetLineWidth(currentContextRef, 5);
    CGFloat lengths[] = {5,1, 3,1, 1,1};
    CGContextSetLineDash(currentContextRef, 0, lengths, 6);
    CGColorSpaceRef colorRef = CGColorSpaceCreateDeviceRGB();
    CGContextSetStrokeColorSpace(currentContextRef, colorRef);
    // component 是指{red, green, blue, alpha};
    CGFloat component[] = {0.0, 0.0 , 1.0, 1.0};
    CGContextSetStrokeColor(currentContextRef, component);
    // 等价于
//    CGContextSetStrokeColorWithColor(currentContextRef, [UIColor blueColor].CGColor);
    CGContextSetFillColorSpace(currentContextRef, colorRef);
    CGContextSetFillColorWithColor(currentContextRef, [UIColor yellowColor].CGColor);
    CGContextAddPath(currentContextRef, mutablePath);
    CGPathRelease(mutablePath);
    CGColorSpaceRelease(colorRef);
    CGContextDrawPath(currentContextRef, kCGPathFillStroke);
}
效果图

剪切举例

- (void)drawRect:(CGRect)rect {
    // 展示整张图
    UIImage * image = [UIImage imageNamed:@"newImage"];
    [image drawInRect:CGRectMake(0, 0, 200, 200)];
    // 设置路径剪切
    CGContextRef currentContextRef = UIGraphicsGetCurrentContext();
    CGContextSaveGState(currentContextRef);
    CGMutablePathRef mutablePath = CGPathCreateMutable();
    CGPathAddRect(mutablePath, NULL, CGRectMake(0, 200, 100, 100));
    CGContextAddPath(currentContextRef, mutablePath);
    CGPathRelease(mutablePath);
    CGContextClip(currentContextRef);
    [image drawInRect:CGRectMake(0, 200, 200, 200)];
    CGRect contextRect = CGContextGetClipBoundingBox(currentContextRef);
    NSLog(@"%@", NSStringFromCGRect(contextRect));
    CGContextRestoreGState(currentContextRef);
    // CGContextClipToRect剪切
    CGContextSaveGState(currentContextRef);
    CGContextClipToRect(currentContextRef, CGRectMake(150, 300, 120, 150));
    [image drawInRect:CGRectMake(0, 200, 200, 200)];
    CGRect contextRect_1 = CGContextGetClipBoundingBox(currentContextRef);
    NSLog(@"%@", NSStringFromCGRect(contextRect_1));
    CGContextRestoreGState(currentContextRef);
}

使用该View

    TextView_1 * view = [[TextView_1 alloc] initWithFrame:CGRectMake(0, 100, 250, 450)];
    [self.view addSubview:view];
    view.backgroundColor = [UIColor redColor];
两个log打印的结果

在这里可以看到第一个log打印结果,即为路径形成的矩形,因为view的size是(250,450),而{{0, 200}, {100, 100}}中所有点都在该范围内。第二个log打印结果并非我们使用CGContextClipToRect函数设置的rect,因为rect为(150, 300, 120, 150),有一部分已经超出了view的范围,取交集即为剪切区域,即{{150, 300}, {100, 150}}。

二、Color and Color Spaces(颜色与颜色空间)

设备(显示器,打印机、扫描仪、摄像机)不会以同样的方式对待颜色,每种设备都有自己的颜色范围使设备可以切实生产。主要了解Quartz是如何描绘颜色与颜色空间的及什么是透明度组件。

1、关于颜色与颜色空间

Quartz 中的颜色是用一组数值来表示。而颜色空间用于解析这些颜色信息,如果没有颜色空间,那么在解析颜色信息时颜色值将毫无意义。常用颜色空间有 :

颜色空间 成分
HSB Hue, saturation, brightness(色相,饱和度,亮度)
RGB Red, green, blue
CMYK Cyan, magenta, yellow, black(蓝绿色,品红,黄,黑)
BGR Blue, green, red

在颜色空间中颜色值范围是相对的,一般在Quartz这种值的范围是(0.0 - 1.0),在Quartz中颜色也有透明度。

2、颜色透明度

alpha为1.0 表示opaque -- 不透明的,为0.0表示无效的,不可见的。在默认混合模式中设置统一的透明度最终显示为:destination = (alpha * source) + (1 - alpha) * destination,你可以单独设置alpha,也可以使用函数CG_EXTERN void CGContextSetAlpha(CGContextRef cg_nullable c, CGFloat alpha)统一设置alpha,如果同时设置了则结果是两个值得乘积。使用 CGContextClearRect(CGContextRef cg_nullable c, CGRect rect) 清除上下文的 alpha 通道。

3、Creating Color Spaces

iOS不支持与设备无关的(device-independent)或通用的(generic)颜色空间(通用颜色空间是设备无关颜色空间的一种)。iOS应用程序必须使用设备(device)颜色空间。

设备无关颜色空间:

颜色空间分类 创建
L ※ a ※ b CGColorSpaceCreateLab
ICC CGColorSpaceCreateWithICCProfile、CGColorSpaceCreateICCBased
Calibrated RGB CGColorSpaceCreateCalibratedRGB
Calibrated gray CGColorSpaceCreateCalibratedGray

通用颜色空间:使用CGColorSpaceCreateWithName(CFStringRef cg_nullable name)函数创建,常用参数如下:

参数名 描述
kCGColorSpaceGenericGray 单个值的范围,从纯黑(0.0)到纯白(1.0)
kCGColorSpaceGenericRGB 三原色组件(0.0 - 1.0)
kCGColorSpaceGenericCMYK 四分量颜色空间(0.0 - 1.0)

设备颜色空间:
 设备颜色空间主要是由iOS应用程序使用。

颜色空间分类 创建
DeviceGray CGColorSpaceCreateDeviceGray
DeviceRGB CGColorSpaceCreateDeviceRGB
DeviceCMYK CGColorSpaceCreateDeviceCMYK

调用 CGContextSetFillColorSpace(context, colorSpace)CGContextSetStrokeColorSpace(context, colorSpace) 设置颜色空间。

使用如下函数设置指定颜色空间的颜色

函数 **
CGContextSetGrayFillColor(CGContextRef cg_nullable c,CGFloat gray, CGFloat alpha)
CGContextSetGrayStrokeColor(CGContextRef cg_nullable c,CGFloat gray, CGFloat alpha)
CGContextSetRGBFillColor(CGContextRef cg_nullable c, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha)
CGContextSetRGBStrokeColor(CGContextRef cg_nullable c, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha)
CGContextSetCMYKFillColor(CGContextRef cg_nullable c, CGFloat cyan, CGFloat magenta, CGFloat yellow, CGFloat black, CGFloat alpha)
CGContextSetCMYKStrokeColor(CGContextRef cg_nullable c, CGFloat cyan, CGFloat magenta, CGFloat yellow, CGFloat black, CGFloat alpha)
CGContextSetFillColorWithColor(CGContextRef cg_nullable c, CGColorRef cg_nullable color)
CGContextSetStrokeColorWithColor(CGContextRef cg_nullable c, CGColorRef cg_nullable color)

还有两个不推荐使用的函数CGContextSetFillColor(CGContextRef cg_nullable c, const CGFloat * cg_nullable components)CGContextSetStrokeColor(CGContextRef cg_nullable c, const CGFloat * cg_nullable components)

4、Setting Rendering Intent(设置渲染意图)

渲染意图指Quartz如何将颜色从源颜色空间范围映射到图形上下文的目标颜色空间的颜色范围内。(例:RGB 颜色空间比 CMYK 的颜色空间要大,一些RGB颜色超出了CMYK颜色空间,若想将某一超出范围的颜色映射到CMYK颜色空间需要遵循的规则即渲染意图)
  调用函数CGContextSetRenderingIntent(CGContextRef cg_nullable c, CGColorRenderingIntent intent)设置渲染意图,其中第二个参数是枚举值:

typedef CF_ENUM (int32_t, CGColorRenderingIntent) {
        kCGRenderingIntentDefault,
        kCGRenderingIntentAbsoluteColorimetric,
        kCGRenderingIntentRelativeColorimetric,
        kCGRenderingIntentPerceptual,
        kCGRenderingIntentSaturation
    };
  • kCGRenderingIntentDefault ----> 默认;
  • kCGRenderingIntentAbsoluteColorimetric ---->绝对色度渲染意图。将输出设备颜色域外的颜色映射为输出设备域内与之最接近的颜色。这可以产生一个裁减效果,因为色域外的两个不同的颜色值可能被映射为色域内的同一个颜色值。当图形使用的颜色值同时包含在源色域及目标色域内时,这种方法是最好的。常用于logo或者使用spot color时。
  • kCGRenderingIntentRelativeColorimetric ----> 相对色度渲染意图。转换包括色域内的所有颜色,以补偿图形上下文的白点与输出设备白点之间的色差;
  • kCGRenderingIntentPerceptual ----> 感性渲染意图。通过压缩图形上下文的色域来适应输出设备的色域,以保持颜色间的视觉关系。感性渲染意图适用于相片及其它复杂的详细的图片(例:bitmap);
  • kCGRenderingIntentSaturation ----> 饱和度渲染意图。当转换到输出设备色域时保持颜色的相对饱和度,结果是包含亮度、饱和度颜色的图片。饱和度意图适用于生成低详细度的图片,如图表、图形的展示;
5、位图图形上下文( bitmap graphics context)支持的像素格式(Pixel Formats)

以下仅列举了iOS 使用的情况:

CS - 颜色空间 Pixel format -像素格式 位图信息常量
Null 8 bpp, 8 bps kCGImageAlphaOnly
Gray 8 bpp, 8 bps kCGImageAlphaNone
Gray 8 bpp, 8 bps kCGImageAlphaOnly
RGB 16 bpp, 5 bps kCGImageAlphaNoneSkipFirst
RGB 32 bpp, 8 bps kCGImageAlphaNoneSkipFirst
RGB 32 bpp, 8 bps kCGImageAlphaNoneSkipLast
RGB 32 bpp, 8 bps kCGImageAlphaPremultipliedFirst
RGB 32 bpp, 8 bps kCGImageAlphaPremultipliedLast

PS:

  • bpp : 像素深度是指存储每个像素所用的位数,它也是用来度量图像的分辨率。像素深度决定彩色图像的每个像素可能有的颜色数,或者确定灰度图像的每个像素可能有的灰度级数。例如,一幅彩色图像的每个像素用R,G,B三个分量表示,若每个分量用8位,那么一个像素共用24位表示,就说像素的深度为24,每个像素可以是16 777 216(2的24次方)种颜色中的一种。
  • bpc : 这个的意思是图片每个颜色所占的bits。

三、Transforms(变换,变形)

Quartz 2D 绘制模型定义了两种独立的坐标空间:用户空间 -- >用于描绘档页和设备空间 -- > 用于描绘设备的本地分辨率。当我们需要一个点或者显示文档时, Quartz 会将用户空间坐标系统映射到设备空间坐标系统。这种当前映射关系的变换矩阵称为 CTM(current transformation matrix)。CTM是关于坐标系的变换,可以独立使用,也可以和仿射变换结合使用(关于仿射变换可以参考这篇文章)。
 在使用CTM做变换时需要在绘制图像之前调用!即,使用CGContextDrawImage (myContext, rect, myImage)函数之前。

1、CTM相关变换函数:
  • 缩放函数 :CGContextScaleCTM(CGContextRef cg_nullable c, CGFloat sx, CGFloat sy)
  • 平移函数 :CGContextTranslateCTM(CGContextRef cg_nullable c, CGFloat tx, CGFloat ty)
  • 旋转函数 :CGContextRotateCTM(CGContextRef cg_nullable c, CGFloat angle)
  • 与仿射变换结合使用的关联函数 :CGContextConcatCTM(CGContextRef cg_nullable c, CGAffineTransform transform)
  • 获取当前图像状态的仿射变换矩阵函数 :CGAffineTransform CGContextGetCTM(CGContextRef cg_nullable c)

需要注意的是:在需要多次使用不同CTM函数时,调用顺序很重要,可能会导致输出结果完全不同!

2、获取用户空间到设备空间的变换

通常当我们使用Quartz 2D绘制时,只在用户空间工作!Quartz 为我们处理用户空间和设备空间的转换。调用如下函数获取 Quartz 转换用户空间和设备空间的仿射变换。

  • 获取用户空间y映射到设备空间的仿射变换: CGAffineTransform CGContextGetUserSpaceToDeviceSpaceTransform(CGContextRef cg_nullable c)
  • 获取点在用户空间上转换到设备空间后的位置 :CGPoint CGContextConvertPointToDeviceSpace(CGContextRef cg_nullable c, CGPoint point)
  • 获取在设备空间上的点在转换到之前在用户空间的位置 :CGPoint CGContextConvertPointToUserSpace(CGContextRef cg_nullable c, CGPoint point)
  • 变换后Size :CGSize CGContextConvertSizeToDeviceSpace(CGContextRef cg_nullable c, CGSize size)
  • 变换前的Size :CGSize CGContextConvertSizeToUserSpace(CGContextRef cg_nullable c, CGSize size)
  • 变换后的rect(通常情况下该函数不保持该矩形,会返回一个包含变换后所有 原rect 角点(四个角上的点~)的最小函数):CGRect CGContextConvertRectToDeviceSpace(CGContextRef cg_nullable c, CGRect rect)
  • 变换前的rect(与上面相同):CGRect CGContextConvertRectToUserSpace(CGContextRef cg_nullable c, CGRect rect)

示例代码:

- (void)drawRect:(CGRect)rect {

    UIImage * image = [UIImage imageNamed:@"newImage"];
    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    CGContextSaveGState(currentContext);
    CGContextTranslateCTM(currentContext, 100, 100);
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(2, 2);
    CGContextConcatCTM(currentContext, scaleTransform);
    CGContextRotateCTM(currentContext, M_PI_4);
    CGContextDrawImage(currentContext, CGRectMake(50, 0, 100, 100), image.CGImage);
    CGContextRestoreGState(currentContext);
    
    // 下面只是调换了一下CTM函数的调用顺序
    CGContextSaveGState(currentContext);
    CGContextRotateCTM(currentContext, M_PI_4);
    CGContextTranslateCTM(currentContext, 100, 100);
    CGContextConcatCTM(currentContext, scaleTransform);
    CGContextDrawImage(currentContext, CGRectMake(50, 0, 100, 100), image.CGImage);
    CGContextRestoreGState(currentContext);
}
效果图
iOS基础
Web note ad 1