1.初览
在正式开始前先简单介绍下CoreGraphics,我会把CoreGraphics分解成概念和套路两个部分,具体框架设计思路和API用法不会涉及,可移步参考Quartz 2D Programming Guide
- 概念
概念好比武学中的内功心法,理解了这些,才能发挥出招式真正的威力,CoreGraphics中比较核心的概念是Graphics Contexts,这将是本文引入的唯一的一个概念。
- Graphics Contexts(图形上下文)
Context是个比较抽象的东西,它不仅仅是一个可以绘制的图层,还包含为当前图层设置的参数,如阴影,线条粗细,绘制模式等。可以类比成一个新建的Photoshop图层以及当前笔触,颜色等配置。对于移动平台,有三种常见的Context
1.View Graphics Context: 由UIView自动创建,你重写UIView drawRect方法时,你的内容会画在这个上下文上。
2.Bitmap Graphics Context: 绘制在该上下文的内容会以点阵形式存储在一块内存中。简单说,就是为图片开辟一块内存,然后在里面画东西,上下文帮你把图片内存抽象成一个Context(图层)了。
3.PDF Graphics Context:顾名思义,跟PDF文件相关,本文不会涉及。
- 套路
就是惯用套路,这相当于武学中的招式。我一直有个疑问,如果一门武学没有招式,怎么判断这是哪个门子的武学? CoreGraphics里面就有相关的招式,这里将带入几个具备代表性的招式,以后看到别人用CoreGraphics写的看似无招的代码,其实仔细品读后会发现所谓的无招只是没有固定套路,招式还是在的。
第一招:拿取当前Graphics Context。
CGContextRef context = UIGraphicsGetCurrentContext();
通常是起始招式,接下来一般会用来为上下文设置参数比如说设置画线时的宽度CGContextSetLineWidth(context, 1),把上下文内容截取成一个位图CGBitmapContextCreateImage(context)等等。
第二招:开辟Bitmap Graphics Context
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0);
draw something ...
UIGraphicsEndImageContext();
注意上下构成一套组合,是开启然后关闭一个Bitmap Graphics Context,在这区域内的所有绘制操作都是对于该Bitmap Context的,在代码块里面使用第一招UIGraphicsGetCurrentContext()拿到的就是这个Bitmap Graphics Context。
第三招: 保存和恢复当前Context状态
Set line width 5, black hair color ...
Draw hair...
CGContextSaveGState(context);//save line width 5, black color
Set line width 8, red color...
Draw hair ornaments...
CGContextRestoreGState(context);//restore line width 5, black color
Continue to draw hair...
这招又是一个组合块,会产生什么效果呢?举个栗子,你正在描绘一个人物的头部画像,画头发=>画装饰物=>修饰头发=>修饰装饰物...这样需要来回切换着画笔状态,实际过程中会有很多参数需要配置,这个招式让我们能保存恢复某些状态。当你使用CGContextSaveGState后接下来你更改画笔状态,画完后再使用CGContextRestoreGState可以将状态恢复到使用Save方法之前。关于哪些状态可以保存,请参考CGContextSaveGState Discussion部分
最后一招:扭转乾坤🙃️
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, CGRectGetHeight(rect)));
这是你熟悉又陌生的线性变换操作,因为Core Graphics(原点左下角,y轴向上为正)使用的坐标系和UIKit(原点左上角,y轴向下为正)的坐标系是不一样的,在重写UIView drawRect的时候直接画上去的内容是一个相对x轴的镜像。因此需要做一次线性变换来得到正确的方位,该操作是将y变成-1*y 然后沿y轴平移,平移距离为CGRectGetHeight(rect)。
好了招式都介绍完了。
2.绘制
接下来要把学到的内容用于实战了,希望通过实战演练,大家能加深理解,逐渐达到无招的状态。代码长度不一,只列出关键部分,文底有全套实现地址。
-
发光
效果图:
实现:
//为文字设置阴影
CGContextSetShadowWithColor(context, CGSizeZero, self.glowSize, self.glowColor.CGColor);
第一招拿到context后,这个效果的核心代码就只有1句了,我都不好意思做解释。你甚至可以直接修改UILabel自带的阴影属性来达到这个效果。
-
描边
效果图:
实现:
//设置边线宽度
CGContextSetLineWidth(context, self.outlineWidth);
//设置线条转角样式
CGContextSetLineJoin(context, kCGLineJoinRound);
//设置绘图模式为描线
CGContextSetTextDrawingMode(context, kCGTextStroke);
这个效果的核心只有3行,通过第一招拿到context,然后配置context。接下来调用super的draw方法,这时候就画了字的描边。如果要如图一样的黑色填充,把DrawingMode改成Fill的模式,再调用一遍super的draw方法即可。
-
渐变
效果图:
实现:
//>>>第一部分
//第二招开始
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0);
//把当前内容绘制在Bitmap Context上
[super drawTextInRect:rect];
//第一招
CGContextRef context = UIGraphicsGetCurrentContext();
//以当前context内容生成一张图片
CGImageRef mask = CGBitmapContextCreateImage(context);
//第二招结束
UIGraphicsEndImageContext();
//>>>第二部分
//第一招,此时是View Graphics context了
context = UIGraphicsGetCurrentContext();
//最后一招,扭转乾坤
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, CGRectGetHeight(rect)));
//注意是ClipTo,因此只有mask部分能被绘制上内容
CGContextClipToMask(context, rect, mask);
//>>>第三部分
//创建渐变
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)color, NULL);
//绘制渐变,渐变只显示mask部分
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, kCGGradientDrawsBeforeStartLocation|kCGGradientDrawsAfterEndLocation);
这个效果大体分为3个部分,第一部分把原来的字形画在一张图片里用作mask。第二部分使用mask裁剪当前view的context。第三部分在当前view的context上绘制一个线性渐变。除线性渐变外,还有径向渐变,具体可以参考结尾的Git代码。
-
镂空
效果图:
实现:
//第一部分
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0);
[super drawTextInRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGImageRef image = CGBitmapContextCreateImage(context);
UIGraphicsEndImageContext();
//第二部分
context = UIGraphicsGetCurrentContext();
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, CGRectGetHeight(rect)));
//使用第一部分得到的image创建一个mask,这里得到的是一个反向的遮罩
CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(image), CGImageGetHeight(image), CGImageGetBitsPerComponent(image), CGImageGetBitsPerPixel(image), CGImageGetBytesPerRow(image), CGImageGetDataProvider(image), CGImageGetDecode(image), CGImageGetShouldInterpolate(image));
CGContextClipToMask(context, rect, mask);
//第三部分
//设置为填充色
[self.maskColor set];
//使用颜色填充区域
CGContextFillRect(context, rect);
该实现跟渐变大体相似,不过这里创建了一个反向的mask,把字体区域给镂空了。然后其他部分填充为一个纯色,当然也可以用图片填充这个区域。还有一种镂空的实现是通过修改BlendMode剔除像素,这里不赘述了。
-
3D
效果图:
实现:
//循环绘制text到context,每次偏移几个像素
[self.text drawInRect:CGRectMake(rect.origin.x + i, rect.origin.y + i, rect.size.width, rect.size.height) withAttributes:attrs];
从动画中应该也可以看出端倪,所谓的3D效果其实是很多图层叠加起来的,因此真正的核心代码只有这一句。但相应的为了实现这种透视感,每一层的颜色和阴影都有些许变化。
-
涂层
效果图:
实现:
//循环绘制图片,图片偏移i = i + randomValue
[self.strokeTexture drawInRect:CGRectMake(i, midY - self.strokeWidth/2.f, self.strokeWidth, self.strokeWidth) blendMode:kCGBlendModeNormal alpha:self.maskAlpha];
这个效果的核心代码也只有这一句,基本思想是用一张笔刷灰度图,修改该灰度图的TintColor,然后绘制在context上,通过随机调整间隔,就达到了深浅相间的效果。控制文字和图片的绘制顺序,就形成了上下效果。
-
故障
效果图:
实现:
这个效果的实现没有引入什么新的操作,是一些基本操作的组合。
1.用3中颜色先把文字画3遍
CGRect bottomRect = CGRectMake(rect.origin.x + self.bottomOffset.x, rect.origin.y + self.bottomOffset.y, rect.size.width, rect.size.height);
CGRect middleRect = CGRectMake(rect.origin.x + self.middleOffset.x, rect.origin.y + self.middleOffset.y, rect.size.width, rect.size.height);
self.textColor = self.bottomColor;
[super drawTextInRect:bottomRect];
self.textColor = self.middleColor;
[super drawTextInRect:middleRect];
self.textColor = self.topColor;
[super drawTextInRect:rect];
得到下面的效果
2.为图片添加切片
//得到切片图片
CGImageRef sliceRef = CGImageCreateWithImageInRect(contentImage, imageSlice);
//把原上下文切片部分内容剔除
CGContextClearRect(context, contentSlice);
//把切片画到原上下文被剔除部分,左右随机平移一定距离
CGContextDrawImage(context, translatedRect, rotateRef);
3.添加随机的线段
代码还有很大优化空间,不列举了,说下基本实现思路吧
- 构建一个循环体
- 循环体内随机生成CGRect
- 过滤掉重叠的Rect
-
材质
效果图:
实现:
这不是蒙图实现!这不是蒙图实现!这不是蒙图实现!蒙图很难实现这种有高低落差的光影效果。
//使用CIHeightFieldFromMask生成高低落差图
CIImage *inputImage = [CIImage imageWithCGImage:imageRef];
CIFilter *filter = [CIFilter filterWithName:@"CIHeightFieldFromMask"];
[filter setValue:inputImage forKey:kCIInputImageKey];
CIImage *outputImage = filter.outputImage;
CGImageRelease(imageRef); imageRef = NULL;
//使用CIShadedMaterial拼接材质
CIImage *materia = [CIImage imageWithCGImage:self.materiaImage.CGImage];
CIFilter *filterMaterial = [CIFilter filterWithName:@"CIShadedMaterial"];
[filterMaterial setValue:outputImage forKey:kCIInputImageKey];
[filterMaterial setValue:materia forKey:kCIInputShadingImageKey];
CIImage *finalEffect = filterMaterial.outputImage;
UIImage *finalImage = [UIImage imageWithCIImage:finalEffect];
[finalImage drawInRect:rect];
通过使用不同的材质球,来显示不同的材质效果,上图使用的材质图
这个效果主要用到的是Core Image里面的filter,目的是引入实现特殊字体的另一个思路,通过第二招将文字变成图片,然后就可以组合使用Core Image Filter来实现更加复杂的效果。CoreImageFilterReference
附上代码地址ArtFontDemo