Inkpad绘图原理浅析

Stella981
• 阅读 753

Inkpad是一款非常优秀的iPad矢量绘图软件,保管你一看见就忘不了。我的感觉是”一览众山小”、”相见甚晚”,以至于我写的TouchVG就是”小巫见大巫”。必须好好学习这款软件的代码,破解其高性能绘图奥秘。

如果你阅读本文觉得哪里写得不明白,可以提出来交流,如果本文能帮助你一点点就OK了,我也是在学习,本意不是想写漂亮的文章。

一、触摸交互绘图

交互式绘图当然得先看触摸响应机制,先看一张序列图:

Inkpad绘图原理浅析

WDCanvas是代表绘图画布的主视图,直接响应原始触摸事件(touchesBegan、touchesMoved等),没有使用双击、旋转、长按等很流行的手势识别器,奇怪吧?
我猜想Inkpad不使用手势识别器有两个原因:(1)手势识别器采用延迟识别技术进行手势二义性判断,有几百毫秒的延迟,会影响绘图快速响应的感觉;(2)Inkpad是独立的程序,所有界面都是自己的,不需要与各种带有手势识别器的界面组件共存(例如在滚动视图、电子书页面中绘图)。     

在WDCanvas中通过”[[WDToolManager sharedInstance].activeTool touchesBegan:touches withEvent:event inCanvas:self]”向当前命令传递触摸事件,这当然用的是_单实例模式_和_命令模式_了。  

 
在WDCanvas的touchesBegan函数中不立即向交互命令WDTool发送触摸开始事件,而是延迟到touchesMoved才去发送。我猜想作者本想区分普通轻击(Tap)和拖动(Pan),但在实现时并不彻底:在下面的touchesEnded函数片段中,没有移动也是要发送touchesBegan的。我认为应该在touchesMoved中检查移动距离来判断是否算是已移动,而不是简单的直接切换到已移动状态。

if (!controlGesture_ && [self canSendTouchToActiveTool]) {
    if (!moved_) {
        [[WDToolManager sharedInstance].activeTool touchesBegan:touches withEvent:event inCanvas:self];
    }
    [[WDToolManager sharedInstance].activeTool touchesEnded:touches withEvent:event inCanvas:self];
}

在开始触摸、当前不是钢笔命令(WDPenTool)时,设置WDCanvas的activePath为空(self.drawingController.activePath = nil)。当前路径(activePath)用于记忆钢笔命令的当前图形路径,将activePath定义在WDDrawingController中而不是定义在WDPenTool中,是方便于在多个类中检查使用,否则要到特定的命令类获取数据太麻烦了。

交互命令类的触摸响应函数使用了如下所示的_模板方法_模式,具体的命令类重写beginWithEvent、moveWithEvent、endWithEvent函数实现具体的绘图逻辑。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas
{
    //.....
    WDEvent *genericEvent = [self genericEventForTouch:primaryTouch_ inCanvas:canvas];
    [self beginWithEvent:genericEvent inCanvas:canvas];
    self.previousEvent = genericEvent;
    //......
}
    
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas
{
    //......
    [self moveWithEvent:genericEvent inCanvas:canvas];
    self.previousEvent = genericEvent;
    //......
}
    
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas
{
    [self endWithEvent:genericEvent inCanvas:canvas];
}
    
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas
{
    [self touchesEnded:touches withEvent:event inCanvas:canvas];
}

下面就要说说Inkpad的一大亮点:在动态拖曳绘图中使用OpenGL ES高速绘图了!

在绘图命令(例如可绘制多种图形的WDShapeTool)中,设置WDCanvas的shapeUnderConstruction属性(待构建的图形,WDPath对象),将当前的临时图形(每次都构造一个WDPath对象)传递给WDCanvas。WDCanvas的setShapeUnderConstruction函数将调用WDSelectionView成员的立即绘制函数,WDSelectionView使用OpenGL ES 1.1绘制各种动态图形。用了OpenGL ES,拖动上千图形也回显毫无延迟感觉,这是Inkpad的亮点。

(我觉得光把这段OpenGL ES渲染矢量图形、文字的类提取出来都足够实现一个很炫的动画显示软件了吧)

Inkpad在使用OpenGL ES 1.1绘制动态图形时,采用的优化显示方法有:(1)单点线宽,(2)忽略虚线样式,(3)不填充,(4)缓存图形轮廓路径。Inkpad对显示性能是下了狠工的。

触摸操作完成后就要向文档提交静态图形了:(1)绘图命令在endWithEvent中向当前层(WDLayer)提交新的图形对象([canvas.drawing addObject:path]);(2)WDDrawingController 触发WDCanvas视图的重绘消息;(3)在WDCanvas的drawRect:函数中,遍历各个图形,调用各个对象的renderInContext函数显示所有图形,其实质是将图形的path显示到当前CGContext上(看似普通)。

与动态图形的绘制相比,提交静态图形并没有采用OpenGL ES,而是使用最简单的CGContext显示方式,而且是重画全部图形,没有使用CALayer等高级渲染方式。这就产生一个疑问,大量图形会不会太慢?我还没去做性能分析实验,初步估计Inkpad采用的显示优化方法是:(1)缓存CGPath轮廓路径和填充路径等对象;(2)避免几何计算和对象重构。

Inkpad已经这么好了,就需要重新评估TouchVG最近做的一些显示优化实验,看看这些计算是否有必要:(1)在GCD中异步绘制,使用前台图形列表和后台图形列表避免多线程冲突;(2)在CALayer上提前绘制,在主视图中只贴图(使用renderInContext,利用GPU贴图);(3)每一层图形一个CALayer,避免重绘所有图形;(4)是否有必要使用OpenGL ES 2.0绘制静态图形?

二、相关核心类

Inkpad绘图原理浅析

1、WDCanvas:绘图主视图,包含标尺视图、选择集渲染视图、删除提示线视图,包含交互命令类要用的临时图形对象。

2、WDCanvasController:绘图界面操作类,负责多种UI控件的管理和操作分发。

3、WDDrawingController:负责各种图形的增删改查逻辑。WDCanvasController是外壳功能,WDDrawingController是内核功能。

4、WDDrawing:图形文档类,容纳所有绘图内容。

5、WDLayer:一个层,包含多个图形元素WDElement。

6、WDStylable:可描边、填充的图形的基类。

7、WDAbstractPath:具有矢量路径的图形的基类。

8、WDPath:可指定线端箭头形状的矢量路径的图形类。

9、WDCompoundPath:复合路径的图形类。

10、WDTool:命令基类。

11、WDShapeTool:添加几何形状的命令,可绘制矩形、椭圆、星形、多边形、直线段、螺旋线。这些不同图形的绘图工具是在WDToolManager中构造的。

12、WDPenTool:贝塞尔曲线绘图工具。

13、WDFreehandTool:自由画光滑曲线工具。

对于蓝色部分的Core.Model类,主要基于矢量路径设计了几何图形模型类,好像不包含图形的特征数据(即后续编辑保持形状特征)吧。

我觉得我们可以基于Inkpad对图形种类和交互命令工具进行扩充,做点行业相关的软件来,如果你感兴趣就加入讨论吧。

这两个UML图是用EA画的,InkpadUml.xmi可以导入到其他UML建模工具。

写了半天有点累了,先写到这吧。期望目标是把Inkpad吃透,结合TouchVG和TouchVGAnimation创造出一个新框架或软件:)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
PPDB:今晚老齐直播
【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Wesley13 Wesley13
3年前
P2P技术揭秘.P2P网络技术原理与典型系统开发
Modular.Java(2009.06)\.Craig.Walls.文字版.pdf:http://www.t00y.com/file/59501950(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.t00y.com%2Ffile%2F59501950)\More.E
Stella981 Stella981
3年前
Golang注册Eureka的工具包goeureka发布
1.简介提供Go微服务客户端注册到Eureka中心。点击:github地址(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2FSimonWang00%2Fgoeureka),欢迎各位多多star!(已通过测试验证,用于正式生产部署)2.原理
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这