神笔马良——基于 OpenGL 的涂鸦框架

4,528 阅读9分钟

所有文章已搬迁到个人站点:me.harley-xk.studio,欢迎访问留言

取这个名字有投机取巧的嫌疑,希望能对得起先贤 >_<

这是什么?

MaLiang 是 iOS 平台一个基于 OpenGL ES3 的涂鸦绘图库,使用纯 Swift 实现,支持自定义纹理、压力感应、自动笔触等特性,并且提供了一定的自定义扩展的空间。

这篇文章可以看作是对 Github 上 README 说明的详细扩展和补充说明。

使用

我的理念是尽量制造简单、优雅的东西,虽然有时候要做到这一点其实很难,但是尽量往这方面靠吧。MaLiang 的集成和使用都很简单,我把大量对使用者来说没有什么用也没有必要了解的内部逻辑都隐藏了。当然了,如果你的好奇心很重,可以自己去看源代码。这篇文章也会介绍一些内部实现的思路。

集成

MaLiang 已经推送到了 Cocopods 的官方 repo,所以,你只需要在 Podfile 增加一条 Pod 指令然后 install 就可以在项目中使用了:

pod 'MaLiang'

然后在需要使用的地方引入 Mudule。当然,首先需要编译一下,否在会报找不到 Moudle 的错误

import MaLiang

几个主要的类

1. Canvas

画布是 MaLiang 最基础的组件,所有的涂鸦都发生在 Canvas 上。Canvas 本质上是一个 UIView,所以你可以使用任何你原来创建 UIView 的方法来创建一个画布,并将它添加到你的界面上。

  • 如果你偏好代码流,那么直接调用 UIView 的通用构造函数 init(frame:) 就可以了。

  • 如果你觉得 IB 流才是正道,只要在 xib 或者 storyboard 中拖一个 UIView 到界面上,然后将类名改成 Canvas 后回车就可以了,Xcode 应该会自动将 Module 设置成 MaLiang

Canvas 设置正确的布局约束,然后你就可以开始涂鸦了,比如写一个像下面这样的东东 :)

嗯,想画成这样,确实还缺少一些东西 :)

Canvas 继承自 MLView(ML是 MaLiang 的缩写,不是那个机器学习的东东),MLView 做了几乎所有与 OpenGL 打交道的事情,虽然它被定义成一个 open 的类,但实际使用中基本是用不到的。不过了解一些原理也无伤大雅么~

OpenGL 涂鸦的核心是纹理(Texture),本质上就是沿着手指轨迹,不断地将纹理叠加到画布上的过程。所以能画出什么样的笔迹,完全取决于使用的纹理,以及它的大小、颜色、尺寸等参数。

MLView 初始化之后会使用自带的图片创建一个默认的纹理,这个纹理就是一个简单的不透明的圆点,所以只能画最简单的线条。如果想要画出上图那样的效果,就需要使用相对复杂一点的纹理了。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 设备的压力感应特性,另外在一些不支持压力感应的设备上使用模拟的压力感应。模拟压感依赖手势移动的速度来判断压力的大小,速度越快压力越小。

  • color 颜色

影响笔迹的颜色,实际画出的颜色会计算进 opacity 的值,不过由于纹理之间会叠加,所以相互效果可以基本抵消。你一般不需要为颜色额外指定透明度的值。

  • texture 纹理

texture 是一个非公开属性,实际使用时只需要使用纹理图的 Image 初始化 Brush 对象就可以了,不需要关心 texture 的具体实现。

实际绘制时的颜色是设定的 color 与纹理的颜色混合之后的结果,所以需要保证纹理图是白色的,才能确保绘制正确的颜色。这个问题可能会在未来改善。

texture 实际上是一个 MLTexture 类型的对象,MLTexture 内部分装了纹理相关的 OpenGL 实现,包括创建纹理、切换画笔时的纹理绑定等。

3. Document

Document 不是实现涂鸦的必备组件,它是为了提供一些更加深入的功能而设计的。Document 维护着持有它的画布的所有笔迹数据,依赖这些数据,可以实现撤销和重做功能。这两个功能 MaLiang 已经默认实现。

通过 Document 持有的数据,你还可以轻松实现保存涂鸦数据到文件的逻辑。反过来也可以将保存的数据重新还原成画布图像,这样可以实现跨设备的数据同步功能。

Document 功能默认是没有启用的,需要手动通过代码开启:

canvas.setupDocument()

Document 在运行过程中需要使用一部分硬盘空间来存放临时数据,所以如果设备存储空间不足时,上面的操作会抛出一个异常,为了保证程序的健壮性,建议使用 do-catch 模式来捕获可能的异常情况:

do {
    try canvas.setupDocument()
} catch {
    // do somthing when error occurs
}

计划实现的一些特性

计划中 MaLiang 还存在一些尚未实现的特性,这些特性会在未来逐渐添加进来,当然,你也帮助我实现,然后给我提交 PR :)

  • 撤销 & 重做,目前已经实现

  • 导出图片,已实现

  • 绘制文本到画布中的指定位置

  • 绘制指定的图片到画布中的指定位置

  • 纹理旋转,旋转纹理可以实现一些更加特殊的笔迹效果

由来

MaLiang 起源于多年前的一个涂鸦项目,当时还是基于 Objective-C 和 OpenGL ES1 实现的,OpenGL ES1 对于抗锯齿的支持不是很好,所以涂鸦的效果不怎么敢恭维。并且当时由于太年轻,整个框架的设计和结构都比较凌乱。虽然最后顺利上架了一段时间,不过由于各种各样的原因,整个项目随当时的公司一起无疾而终了。

去年开始重拾这个项目,打算基于 Swift 和 OpenGL ES3 完全重写,同时将当时处理得不是很好的地方加以改进,另外扩展了一些自己近期想到的东西,最终诞生了这个库。

Why Swift?

使用 Swift 直接和 OpenGL 打交道确实不是一件容易的事情,有人奉劝我使用 OC 或者 C 作为中间层来调用 OpenGL,再用 Swift 封装上层逻辑,确实这样可以以最低的成本实现需要的效果。

不过作为一个业余项目,成本并不是我第一考虑的要素,而且这个库虽然是基于 OpenGL 的,但是真正跟 OpenGL 打交道的,其实也就那几百行代码。为了追求这一点点成本和便利性,牺牲整个项目结构的统一和整洁,在我这是无法接受的。

另外,引入 OC 代码意味着同时引入了 OC 的动态运行时环境,这对 Swift 的执行效率会有一定的影响。虽然作为一个 iOS 的项目,现在必然无法摆脱 OC 的动态运行时环境,我的这点偏执似乎也没有什么意义,不过谁知道以后会怎么样呢 :)

应用

说了半天,这个库有什么用?说实话我也不知道,或许可以用来做签名?不过签名其实用 CoreGraphics 就足够了。或许可以用它来做一个画画的 App 来逗小孩玩,可能我真会这么干。。。

说到底,这主要是对当初懵懂时期经历的一个纪念吧。感兴趣的都可以拿去玩 :)

接下来可能会打算基于这个库开发一款涂鸦的 App。当然了,多年前的那个项目是不会复活了,新的这个 App 会是一个融合了很多我自己想法的全新项目。当然了希望不要半途而废 - -!