所有文章已搬迁到个人站点:me.harley-xk.studio,欢迎访问留言
取这个名字有投机取巧的嫌疑,希望能对得起先贤 >_<
MaLiang 现已全面升级到 Metal。
本文已针对最新 Metal 版本更新,最后更新于:2019-05-10
这是什么?
MaLiang 是 iOS 平台基于 Metal 的涂鸦绘图框架,完全使用 Swift 实现,支持自定义纹理、缩放、压力感应等特性,并且提供了较大的自定义扩展的空间。
特色功能
- 笔迹通过贝塞尔曲线进行平滑处理
- 支持添加自定义贴图
- 纹理和贴图支持旋转
- 内置一个荧光笔特效的实现
- 可以根据压力自动调整笔迹,支持 3D Touch
- 支持撤销和重做
- 支持滚动和缩放画布
- 导出绘制内容为图片
- 将绘制内容保存为矢量文档和从文档恢复数据
使用
我的理念是尽量制造简单、优雅的东西,虽然有时候要做到这一点其实很难,但是尽量往这方面靠吧。MaLiang 的集成和使用都很简单,我把大量对使用者来说没有什么用也没有必要了解的内部逻辑都隐藏了。这篇文章也会介绍一些内部实现的思路。
集成
Cocoapods
MaLiang 已经推送到了 Cocopods 的官方 repo,所以,你只需要在 Podfile 增加一条 Pod 指令然后 install
就可以在项目中使用了:
pod 'MaLiang'
如果你想使用老的 OpenGL 版本,请指定到 1.1.x 版本
pod 'MaLiang', '~> 1.1'
需要注意 OpenGL 版本只包含有限的功能并且已停止维护
Carthage
MaLiang 现在也支持通过 Carthage 集成,首先在 Cartfile
中增加指令:
github "Harley-xk/MaLiang"
然后执行 cartage update
命令来完成编译,然后将编译完成的 MaLiang.framework
添加到 Xcode 项目中。
需要确保将 MaLiang.framework
添加到 target 的 Embedded Binaries
中
调用
在需要使用的地方引入 Mudule。当然,首先需要编译一下,否在会报找不到 Moudle 的错误
import MaLiang
几个主要的类
1. Canvas
画布是 MaLiang 最基础的组件,所有的涂鸦都发生在 Canvas
上。Canvas
本质上是一个 UIView
,所以你可以使用任何你原来创建 UIView
的方法来创建一个画布,并将它添加到你的界面上。
如果你偏好代码流,那么直接调用
UIView
的通用构造函数init(frame:)
就可以了。如果你觉得
IB
流才是正道,只要在xib
或者storyboard
中拖一个UIView
到界面上,然后将类名改成Canvas
后回车就可以了,Xcode 应该会自动将 Module 设置成MaLiang
。
给 Canvas
设置正确的布局约束,然后你就可以开始涂鸦了,比如写两个毛笔字:
嗯,请忽略我的书法水平... 想画成这样,还缺少一些东西 :)
Canvas
继承自 MetalView
, MetalView
做了几乎所有与 Metal
打交道的事情,虽然它被定义成一个 open
的类,但是如果不是深度定制的话,应该不需要对它进行扩展。
MaLiang 涂鸦的核心是纹理(Texture
),本质上就是沿着手指轨迹,不断地将纹理叠加到画布上的过程。所以能画出什么样的笔迹,完全取决于使用的纹理,以及它的大小、颜色、尺寸、透明度等等这些参数。
MetalView
初始化之后会创建一个默认的纹理,这个纹理就是一个简单的不透明的圆点,所以只能画最简单的线条。如果想要画出上图那样的效果,就需要使用相对复杂一点的纹理了。MaLiang 的示例项目里面提供了好几个设定好的纹理,用他们可以模拟出铅笔、水笔以及毛笔的特效,上面的文字就是使用毛笔特效写出来的。
快照
Canvas
提供了一个简单的快照功能:
open func snapshot() -> UIImage?
调用该方法会对画布生成一个当前内容的快照并以 Image 的形式返回,快照的实现逻辑很简单,你也可以自己实现更加复杂的快照逻辑。
2. Brush
直接使用纹理还是比较繁琐的,另外与纹理相关的还有颜色、线条的粗细以及其他一些参数,所以这里提供了一个 Brush
类来处理所有的这些数据。
Brush
的属性在改变后会立刻影响接下来的绘制效果。
-
opacity
透明度
上面提到,涂鸦的本质是把纹理叠加到画笔的过程,所以想要做出深浅不一的笔迹,纹理就需要具有透明度,可以通过opacity
属性来调节。
-
pointSize
笔迹粗细
pointSize
直接影响笔迹的粗细,它是以 iOS 尺寸的标准单位 点(point
) 来衡量的,所以这是一个自适应屏幕像素密度的属性。你不需要根据设备类型来计算实际像素,直接指定眼睛可见的大小就可以了。
-
pointStep
点距
同上,由于笔迹是通过叠加纹理实现的,因此除了透明度外,两个纹理之间的距离也会影响到笔迹的深浅。另外如果把点距设定到大于笔迹的尺寸,甚至可以画出类似虚线的效果。点距的单位也是 点(point
)。
-
forceSensitive
压力敏感度
之所以说 pointSize
是影响笔迹的粗细,而不是直接确定,是因为有压力感应的存在。笔迹的实际尺寸会随着压力的大小在 pointSize
指定的尺寸上下浮动,压力越大,笔迹越粗。forceSensitive
影响笔迹对压力浮动的剧烈程度,建议设置为 0
- 1
之间的某个值。如果设置过大,笔迹随压力的便会会太过剧烈而失真;如果将 forceSensitive
的值设置为 0
,则对该画笔关闭压力感应效果,笔迹粗细不会随着压力而变化。
MaLiang 默认使用 iOS 设备的 3D Touch 参数,另外在一些不支持压力感应的设备上使用模拟的压力感应。模拟压感依赖手势移动的速度来判断压力的大小。
-
color
颜色
影响笔迹的颜色,实际画出的颜色会计算进 opacity
的值,不过由于纹理之间会叠加,所以相互效果可以基本抵消。你一般不需要为颜色额外指定透明度的值。
-
texture
纹理
texture
是一个非公开属性,实际使用时只需要使用纹理图的 Image 初始化 Brush
对象就可以了,不需要关心 texture
的具体实现。
对于纯色线条,颜色是根据对画笔所指定的 color
属性来决定的,纹理只需要提供准确的透明度信息即可,因此可以是任意颜色。
3. CanvasData
CanvasData
会在画布创建后自动初始化,它保存了当前画布上的所有笔迹信息,依赖这些数据,目前实现了撤销和重做功能。
通过 CanvasData
持有的数据,还可以轻松实现保存涂鸦数据到文件的逻辑。反过来也可以将保存的数据重新还原成画布图像,这样可以实现跨设备的数据同步功能。MaLiang 提供了一个默认版本的数据保存和读取的逻辑,你可以在此基础上做进一步的封装,也可以完全另外实现你自己的存储逻辑。
MaLiang 进化史
MaLiang 起源于 12 年的一个涂鸦项目,当时还是基于 Objective-C 和 OpenGL ES1 实现的,OpenGL ES1 对于抗锯齿的支持不是很好,所以涂鸦的效果不怎么敢恭维。并且当时由于太年轻,整个框架的设计和结构都比较凌乱。虽然最后顺利上架了一段时间,不过由于各种各样的原因,整个项目随当时的公司一起无疾而终了。
18年业余时间重拾了这个项目,基于 OpenGL ES3,使用 Swift 完全重写,同时对整个项目结构进行了重新设计和改进,并进行了少量的扩展。
然而生不逢时,MaLiang 刚完成没多久,苹果就宣布废除 OpenGL ES,iOS 平台 Metal 成了唯一选项。但由于我对 OpenGL ES 都只是一知半解,Metal 就研究的更少了,将 MaLiang 迁移到 Metal 的计划便一直处于搁浅状态。
直到最近才终于下定决心,从渲染管线到着色器语言,恶补了 Metal 相关的基础知识,终于将底层绘制相关的逻辑全部用 Metal 改写,并且对上层逻辑进行了部分重构。同时顺便将原来基本没有用户体验的缩放功能完全重写,实现了可以放大画布之后绘制微缩图案的功能。
当然了,我对 Metal 的理解还很浅薄,MaLiang* 还是有很多需要优化改进的地方。*
Why Swift?
Swift 从 15 年开始就作为我的主要开发语言了,所以回去写 OC 对我来说已经不可能,尽管之前写 OpenGL 时使用 Swift 确实很麻烦,不过最后还是完成了。
当时有大佬奉劝我使用 OC 或者 C 作为中间层来调用 OpenGL,再用 Swift 封装上层逻辑,确实这样可以以最低的成本实现需要的效果。不过作为一个业余项目,成本并不是我第一考虑的要素,而且这个库虽然是基于 OpenGL 的,但是真正跟 OpenGL 打交道的,其实也就那几百行代码。为了追求这一点点成本和便利性,牺牲整个项目结构的统一和整洁,在我这是无法接受的。
而坚持使用 Swift 的好处在最近迁移到 Metal 时就表现出来了。只需要对几处OpenGL的关键绘制逻辑使用 Metal 改写,就能很快完成迁移,否则就需要重写整个底层逻辑了 :)
另外,引入 OC 代码意味着同时引入了 OC 的动态运行时环境,这对 Swift 的执行效率会有一定的影响。虽然作为一个 iOS 的项目,现在必然无法摆脱 OC 的动态运行时环境,我的这点偏执似乎也没有什么意义,不过谁知道以后会怎么样呢 :)
在这个时间点,Swift 5 刚好发布没多久,终于迎来了 ABI 稳定这一天,这意味着 Swift App 距离脱离 OC 的动态运行时,完全运行在纯 Swift 环境中那一天已经不远了:)
应用
说了半天,这个库有什么用?说实话我也不知道,或许可以用来做签名?不过签名其实用 CoreGraphics
就足够了。或许可以用它来做一个画画的 App 来逗小孩玩,可能我真会这么干。。。
说到底,这主要是对当初懵懂时期经历的一个纪念吧。感兴趣的都可以拿去玩 :)
接下来可能会打算基于这个库开发一款涂鸦的 App。当然了,多年前的那个项目是不会复活了,新的这个 App 会是一个融合了很多我自己想法的全新项目。当然了希望不要半途而废 - -!
基于 MaLiang 的涂鸦 App 已经开发完成并上线,可以在 AppStore 搜索 马良 下载体验。