Core Graphics实现各种艺术字效果

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.绘制

接下来要把学到的内容用于实战了,希望通过实战演练,大家能加深理解,逐渐达到无招的状态。代码长度不一,只列出关键部分,文底有全套实现地址。

  • 发光

效果图:

glow.gif

实现:

//为文字设置阴影
CGContextSetShadowWithColor(context, CGSizeZero, self.glowSize, self.glowColor.CGColor);

第一招拿到context后,这个效果的核心代码就只有1句了,我都不好意思做解释。你甚至可以直接修改UILabel自带的阴影属性来达到这个效果。

  • 描边

效果图:

stroke.gif

实现:

//设置边线宽度
CGContextSetLineWidth(context, self.outlineWidth);
//设置线条转角样式
CGContextSetLineJoin(context, kCGLineJoinRound);
//设置绘图模式为描线
CGContextSetTextDrawingMode(context, kCGTextStroke);

这个效果的核心只有3行,通过第一招拿到context,然后配置context。接下来调用super的draw方法,这时候就画了字的描边。如果要如图一样的黑色填充,把DrawingMode改成Fill的模式,再调用一遍super的draw方法即可。

  • 渐变

效果图:

gradient.gif

实现:

//>>>第一部分
//第二招开始
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代码。

  • 镂空

效果图:

hollow.gif

实现:

//第一部分
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

效果图:

3d.gif

实现:

//循环绘制text到context,每次偏移几个像素
[self.text drawInRect:CGRectMake(rect.origin.x + i, rect.origin.y + i, rect.size.width, rect.size.height) withAttributes:attrs];

从动画中应该也可以看出端倪,所谓的3D效果其实是很多图层叠加起来的,因此真正的核心代码只有这一句。但相应的为了实现这种透视感,每一层的颜色和阴影都有些许变化。

  • 涂层

效果图:

marked.gif

实现:

//循环绘制图片,图片偏移i = i + randomValue
[self.strokeTexture drawInRect:CGRectMake(i, midY - self.strokeWidth/2.f, self.strokeWidth, self.strokeWidth) blendMode:kCGBlendModeNormal alpha:self.maskAlpha];

这个效果的核心代码也只有这一句,基本思想是用一张笔刷灰度图,修改该灰度图的TintColor,然后绘制在context上,通过随机调整间隔,就达到了深浅相间的效果。控制文字和图片的绘制顺序,就形成了上下效果。

  • 故障

效果图:

glitch.gif

实现:
这个效果的实现没有引入什么新的操作,是一些基本操作的组合。
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];

得到下面的效果

step1.gif

2.为图片添加切片

//得到切片图片
CGImageRef sliceRef = CGImageCreateWithImageInRect(contentImage, imageSlice);
//把原上下文切片部分内容剔除
CGContextClearRect(context, contentSlice);
//把切片画到原上下文被剔除部分,左右随机平移一定距离
CGContextDrawImage(context, translatedRect, rotateRef);
step2.gif
step2_2.gif

3.添加随机的线段
代码还有很大优化空间,不列举了,说下基本实现思路吧

  • 构建一个循环体
  • 循环体内随机生成CGRect
  • 过滤掉重叠的Rect
step3.gif
  • 材质

效果图:

materia.png

实现:
这不是蒙图实现!这不是蒙图实现!这不是蒙图实现!蒙图很难实现这种有高低落差的光影效果。

//使用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];

通过使用不同的材质球,来显示不同的材质效果,上图使用的材质图

Golden.png

这个效果主要用到的是Core Image里面的filter,目的是引入实现特殊字体的另一个思路,通过第二招将文字变成图片,然后就可以组合使用Core Image Filter来实现更加复杂的效果。CoreImageFilterReference

附上代码地址ArtFontDemo

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

推荐阅读更多精彩内容