iOS白板同步绘制方案调研

Apple Pencil绘制原理

  • Apple Pencil 可以额外采集到 altitude(高度角)、azimuth(方位角)、force(压感值)
  • 关于force压感值(3D Touch):force乘以一个压感系数sensitivity(自定义),计算一个两点之前的线宽linewidth(force * sensitivity),来实现压力不同线宽不同功能
  • 关于altitude角度:一般用来绘画阴影部分,如角度小于30°的时候,可以模拟素描阴影的绘制
  • 关于合并:Apple Pencil的捕获频率为240Hz,UIKit一般捕获触摸的频率为60Hz,这意味着Apple Pencil可以捕捉到更多的点,UIKit会把Apple Pencil捕获的多余的点合并到最后一个触摸点的UITouch对象里。
  • 关于预估:iOS9以后UITouch可以实时返回预估的笔迹

以上的场景,使用force、altitude的计算、或处理合并的所有点均会增加绘制过程中的点阵数据包的大小,同时绘制的系数也需要调优减少可能造成的锯齿,如果是互动白板传输数据的场景(如腾讯在线课堂、网易白板等),都没有使用压感和角度的功能。如果是本地高精度的绘画应用,均是可以实现的。具体是否需要可以根据后续需求来定。后续会针对这一块(使用force、altitude、合并、预估笔迹)做预研和压力测试是否可行,增大笔迹包流量下的性能、消息丢失统计、体验、延时等的性价比综合对比分析

详情见:Handling Input from Apple Pencil

iOS端白板绘制实现方案

调研了目前网上白板的实现方式,基于不考虑压感值和角度的采集的情况下,都是基于UIResponder实现的,考虑优先实现一个画板的基础功能(撤销、恢复、橡皮等),实现方案有以下几种:

1.UIBezierPath➕drawRect实现

优点:实现简单
缺点:实现撤销、恢复等功能需要重绘调用drawRect,在累计笔迹大量的情况下,频繁刷新drawRect会导致内存过大

2.Quartz2D➕drawRect实现

优点:UIBezierPath其实是Quartz2D的封装,实现简单
缺点:因为重写了drawRect的方法,和方案一同样有笔迹过多的情况下内存过大的问题

3.UIBezierPath➕CAShapeLayer实现

优点:撤销、恢复等功能实现简单,内存小
缺点:橡皮实现较为复杂,需要注意优化计算量

4.OpenGLES实现

优点:实现自由度最高,性能理论可以调到最优
缺点:开发复杂,学习成本高,所有功能(橡皮、撤销、恢复、矩形等)需要采坑实现及调优

参考了大部分源码,一个高性能的白板基本采用方案三 UIBezierPath➕CAShapeLayer 实现

画板绘制流程

采集

每一个笔迹在开始、结束、移动的过程中有起始点B、终点E及移动点阵M1,M2,…,MN存在,iPad端绘制的同时会采集这些点阵通过NATS传输供学生端绘制,采集的通用格式为:

{
    "n":1, // 序号 no
    "c":1, // 笔画颜色 color
    "l":1, // 笔画宽度 line
    "s":0, // 分片数量 sub number
    "o":0, // 分片序号 sub order
    "e":0, // 画线类型0默认 1直线 2椭圆 3矩形 4橡皮擦等
    "a":0, // 操作类型action: 1 正常绘图 2 撤销 3 恢复 4 清空等
    "t":10000, // 时间戳 timestamp
    "w":1024, // 画板高度
    "h":768, // 画板宽度
    "d":[{"x":10.5,"y":10.5},{"x":20.5,"y":20.5}]  // x,y为坐标
}

其中,d的数组实际就是

[B,M1,M2,...,MN,E]

针对每个笔迹,归类为以下绘制方式,值为e(0默认 1直线 2椭圆 3矩形 4橡皮擦):
默认笔画(曲线):对相邻的点做贝塞尔曲线(BezierPath),BezierPath分多个阶段,高阶的曲线更圆滑

贝塞尔曲线说明

贝塞尔曲线
Apple UIBezierPath

曲线的优化计算

1.移动到起始点 N1
2.当累计点到N2,N3,N4的时候,计算N2和N4的中点N3, 覆盖原来的N3点;
3.以N1为起始点对N3画一条二阶贝塞尔曲线,控制点为N2和N4
4.将N3做为起始点N1,N4作为第一个累计点N2,回到第一步

详细参考:Smooth Freehand Drawing

直线:取第一个点B和最后一个点E,画一条直线
矩形:取第一个点B和最后一个点E,范围内画一个矩形,需要注意起始点和终点的向量方向
椭圆:取第一个点B和最后一个点E,范围内画一个椭圆,需要注意起始点和终点的向量方向
点:当B和E的坐标相等时,就画一个点

动作,值a(1 正常绘图 2 撤销 3 恢复 4 清空等):
撤销:pop撤销栈最后一个临时图片并push恢复栈,渲染
恢复:pop恢复栈最后一个临时图片并push撤销栈,渲染
清空:清空所有栈及当前画板

点阵分片及重组

笔迹传输是按照每个笔迹采集的,如果一个笔迹的点阵数量过大,可以分片处理,NATS消息体里面会表示s和o表示分片的数量及序号,接收端需要收到全部分片后进行点阵d的数组重组及绘制。

时间戳

需要和视频流里面的时间戳消息做对比,小于视频流时间戳的时候才显示笔迹。