我们的项目是面向学校老师的教学软件,所以肯定少不了互动白板的功能,而这个里面的画笔功能是由我来开发的,下面介绍这个过程中遇到的问题以及解决方法。

首先给大家明确下由于软件中的画布可以自由移动,会超出屏幕显示范围,同时支持点擦和线擦,所以需要存储所有点坐标。

第一版简单画笔实现并优化掉折线感

领券网 https://www.cps3.cn/

第一版实现的肯定是很简单的画笔线条,由给定的鼠标坐标位置连线画出线段,主要使用的canvas的API方法有:beginPath moveTo  lineTo stroke。不过很快发现当鼠标快速画曲线时出现很明显的拐点,这里要用到贝塞尔曲线来解决,具体可参考《利用贝塞尔绘制平滑曲线》。

第二版解决快速画线时笔迹跟不上鼠标移动的问题

实现了贝塞尔曲线的绘制,同时也产生新的问题,绘制过程中会出现线条的延长跟不上鼠标的情况(这是由于贝塞尔曲线的应用引起的,二次贝塞尔曲线绘制的时候需要三点确定起始点和控制点,《利用贝塞尔绘制平滑曲线》有具体讲解,看懂就能明白为什么会跟不上了)。

由于我们存储了所有点坐标,所以解决这个题也好办,就是mousemove触发绘制时都遍历一遍本条线上所有点来绘制这条线。

所以每次鼠标移动采用的绘制过程是先清除画布,再绘制整条笔迹。当然这里我们已经采用了一个优化性能的方式,就是分层canvas,绘制中的画笔笔迹使用drawingCanvas,当鼠标释放确定了一条线后,这条线会移动到主画布mainCanvas上,达到动静分离。这样每次取出当前线条的所有点坐标,利用贝塞尔绘制出平滑的曲线。并绘制到最后一个鼠标点位置处,解决跟不上鼠标移动的问题。

第三版解决点擦和线擦不连续的问题

我们实现的橡皮擦除并不是像大家熟悉的方式设置globalCompositeOperation,去盖住原有图形的方式。《清除canvas画布内容--点擦除+线擦除》有详细介绍我们的方法,主要采集鼠标滑过的点利用canvas缓存颜色的图形拾取方式来找到要擦除的图形及具体应该去掉哪几个坐标点,或者哪条线。但是这样如果鼠标滑动很快的话,两个mousemove触发的间隔距离就会很大,那么中间的线都不会被擦除掉。针对这个问题,主要采用了中间补点的方式来模拟增加采集鼠标点的密度。

 1                 //橡皮优化,鼠标快的时候擦除不干净
 2                 let dis = XlMath.getInstance().distance(that.eraserLastPoint, p);
 3                 // let isDraw = false;
 4                 if (dis > eraserRadius) {
 5                     let basePoint = that.eraserLastPoint;
 6                     for (let i = 0; i < 1000; i++) {
 7                         basePoint = new Point((p.x - that.eraserLastPoint.x) * eraserRadius / dis + basePoint.x, (p.y - that.eraserLastPoint.y) * eraserRadius / dis + basePoint.y);
 8                         if ((basePoint.x - p.x) * (that.eraserLastPoint.x - p.x) < 0 || (basePoint.y - p.y) * (that.eraserLastPoint.y - p.y) < 0)
 9                             break;
10                         else {
11                             let eraserReturn = that.eraser(basePoint);
12                             if (eraserReturn) {
13                                 editor.courseware.draw(false, true);
14                                 if (currentEditMode == EditMode.elementEraser)
15                                     editor.bdCanvas.drawPenStatusForElement(true);
16                             }
17                         }
18                     }
19                 }

View Code

第四版增加笔锋效果

我们的用户反馈别人家的app会有笔锋效果,写出的字就很漂亮,我们能不能也加上。但据我们调查,很漂亮的笔锋效果都是用底层的.net组件或者其他底层语言实现的。但我们也硬着头皮想方法,实现了并不是太完美的笔锋效果,如下图

手写笔迹效果有两个关键点:落笔,收笔

1 落笔效果

落笔的地方先绘制个椭圆,椭圆的方向根据前两个点的角度确定:

 1   //计算角度
 2             ctx.beginPath();
 3             ctx.fillStyle = this.renderStyle.strokeColor;
 4             let dire = Util.GetSlideDirection(points[0].x, points[0].y, points[1].x, points[1].y, false);
 5             if (dire == 1) {//向上
 6                 ctx.ellipse(points[0].x, points[0].y + 1.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 5.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 4 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, Math.PI / 4, 0, Math.PI * 2);
 7             } else if (dire == 2) {//向下
 8                 ctx.ellipse(points[0].x, points[0].y - 1.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 5.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 4 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, Math.PI / 4, 0, Math.PI * 2);
 9             } else if (dire == 3) {//向左
10                 ctx.ellipse(points[0].x + 1 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, points[0].y - 0.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 5.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 4 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, Math.PI * 5 / 4, 0, Math.PI * 2);
11             } else {
12                 ctx.ellipse(points[0].x - 1.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, points[0].y, 5.5 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, 4 / 10 * this.renderStyle.lineWidth / ctx.scaleVal, Math.PI / 4, 0, Math.PI * 2);
13             }
14             ctx.fill();

2 收笔效果

落笔处的一段线条的线宽需要动态变化,制造慢慢变细的效果(用到了贝塞尔补点):

 1 let maxLineWidth = this.renderStyle.lineWidth;
 2             let minLineWidth = this.renderStyle.lineWidth / 3;
 3             let pointCounter = 0;
 4             let points: Array<Point>;
 5             if (isUp||this.penType != 1)//不是需要绘制笔锋的线条类型 或者鼠标松开时
 6                 points = this.points;
 7             else
 8                 points = Util.clone(this.points);
 9             //当前绘制的线条最后笔锋处补点 贝塞尔方式增加点
10             if (this.penType == 1 && points.length >= 2) {
11                 let i = points.length - 1;
12                 let endPoint;
13                 let controlPoint;
14                 let startPoint = points[i];
15                 let allInsertPoints = new Array<Point>();
16                 while (i >= 0) {
17                     endPoint = startPoint;
18                     controlPoint = points[i];
19                     if (i == 0)
20                         startPoint = points[i];
21                     else
22                         startPoint = new Point((points[i].x + points[i - 1].x) / 2, (points[i].y + points[i - 1].y) / 2);
23                     if (startPoint && controlPoint && endPoint) {//使用贝塞尔计算补点
24                         let dis = (XlMath.distance(startPoint, controlPoint) + XlMath.distance(controlPoint, endPoint)) * ctx.scaleVal;
25                         let insertPoints = XlMath.bezierCalculate([startPoint, controlPoint, endPoint], Math.floor(dis / 6) + 1);
26                         // 把insertPoints 变成一个适合splice的数组(包含splice前2个参数的数组,第一个参数要插入的位置,第二个参数要删除的原数组个数)
27                         insertPoints.unshift(0, 0);
28                         Array.prototype.splice.apply(allInsertPoints, insertPoints);
29                         points.pop();
30                     }
31                     pointCounter++;
32                     if (pointCounter >= 6)
33                         break;
34                     i--;
35                 }
36                 //赋值最后几个点的线宽
37                 let insertCount = allInsertPoints.length;
38                 for (let i = 0; i < insertCount; i++) {
39                     let w = (maxLineWidth - minLineWidth) / insertCount * (insertCount - i) + minLineWidth;
40                     allInsertPoints[i].setLineWidth(XlMath.toDecimal(w));
41                     points.push(allInsertPoints[i]);
42                 }
43             }

View Code

有了这个效果,代价就是性能了。

几个耗费性能的点:

1)因为一条线段的结尾处在不断变化设置lineWidth;同时也需要多次调用stroke接口

2)使用椭圆api

3)中间计算线宽以及用贝塞尔补点的过程

第五版去笔锋优化画笔流畅度

后来证明对于学校老旧电脑来说,用户流畅度的需求大过于线条的美观度。所以我们又恢复了原来的绘制方式,去掉了笔锋效果。同时从事件响应,收集鼠标坐标点上也做了优化。

对于第二版的优化去掉折线感后带来的鼠标移动笔迹跟不上的问题,我的解决方案每次绘制整条线是有一定的性能影响的,我也曾建议在绘制过程中在drawingCanvas上面绘制的线条容许有折线,鼠标释放笔迹成型后优化掉折线绘制到mainCanvas上,但产品不太接收。后来妥协的接受方式是绘制中笔迹并不能紧跟鼠标的效果。

所以来来回回最后取消了第二次和第四次的改版实现,这个过程也是在平衡笔迹外观和性能的过程,哪个对用户更重要,就往哪个方向改进。

擦除流畅性的限制

前几久产品又提出我们擦除上面的不流畅,不如其他软件的真实流畅,据此我也调研了几种方案:

  1. 可以将整个canvas画布转化成base64编码的image(调用api方法toDataURL),后面再次绘制的时候把这个image数据再绘制到canvas上,可以继续在这个canvas上进行绘制和擦除内容。但我们黑板的画布是可移动的,所以这个方法会丢掉屏幕之外的线条笔迹,另外线擦除无法使用
  2. 将画布每个像素点rgb保存到课件(使用api方法getImageData),但存储范围也仅限可视区域,我们黑板的画布是可移动的,所以这个方法会丢掉屏幕之外的线条笔迹,另外线擦除无法使用
  3. 为解决上面两种方法造成屏幕置为笔迹丢失问题,我们使用globalCompositeOperation设置成destination-out的擦除方法(可以理解成覆盖书写),同时保存拖拽擦除时鼠标经过的点,也就是按照画笔线条的方式 另外保存一份擦除线条的点集合,这个方法会造成课件体积变大,需要数据库支撑,另外也不能实现线擦除,一条线被从中间擦除仍然还是一条线(需要的效果是两条单独的线了),所以会出现擦除混乱的情况。
    总结,基于我们业务的复杂性,画布实际上很大可平移,有点擦除和线擦除,只能采用目前的实现方式

记canvas画笔笔迹的多次优化过程相关推荐

  1. 对Group By 语句的一次优化过程

    对Group By 语句的一次优化过程 对Group By 语句的一次优化过程 作者: fuyuncat 来源: www.HelloDBA.com 生产环境中发现一条语句很慢,拿回来一看,其实是一个简 ...

  2. 记一次网站无法访问解决过程,服务器80端口问题解决过程

    记一次网站无法访问解决过程,服务器80端口问题解决过程 参考文章: (1)记一次网站无法访问解决过程,服务器80端口问题解决过程 (2)https://www.cnblogs.com/slyzly/a ...

  3. html5笔迹画图,html5绘图工具canvas模拟笔迹绘画特效

    特效描述:html5绘图工具 canvas 模拟笔迹 绘画特效.html5绘图工具使用鼠标在网页上进行写字,canvas绘画模拟笔迹特效 代码结构 1. HTML代码 sorry! //定义获取id ...

  4. 记一次打包源码的过程

    记一次打包源码的过程  黑客攻防  Panni_007  2013-06-28  401浏览  0评论 http://panni007.com/2013/06/28/1228.html 0×01 起因 ...

  5. Canvas画笔的基本使用

    文章目录 Canvas画笔的基本使用 图形绘制 设置样式 画笔实例练习 渐变色绘制 镂空的房子 绘制坐标网格 绘制坐标系 绘制坐标点 绘制折线图 参考文档 Canvas画笔的基本使用 图形绘制 需要理 ...

  6. 【Android开发基础】Canvas画笔(以刮刮乐为例)

    文章目录 一.引言 二.设计 1.获取图片资源 2.获取屏幕信息 3.Canvas涂层 4.随机内容 5.屏幕监听 三.附件 1.UI设计 2.总代码 (1)控件初始化 (2)图层初始化 3.源代码 ...

  7. android 绘画笔迹回放_Android画板 半透明画笔 笔迹叠加效果

    0.写在前面 先看下效果图,功能虽然简单,但是实现的时候谷歌.百度了很久也没有找到解决方案,提这个问题的人不少,但是回答的人一个也没有,十分郁闷,在此记录,分享给各位. 叠加效果 1.半透明画笔 先按 ...

  8. Android之Canvas画笔和画布

    久违的Canvas画布,终于学到这里了,学完以后附上博文一篇以便日后记不住. 目录 一.Canvas(画布) 二.Paint(画笔) 三.实例 涉及的相关知识点 1.绘制安卓机器人 2.绘制文本 3. ...

  9. canvas画笔自定义笔触

    1.先上图: 2.核心代码: //<image src="./images/penStyle.ico" id="penStyle"></ima ...

最新文章

  1. 这25条极简Python代码,你还不知道
  2. 字节内部前端开发手册(完整版)开放下载!
  3. 分治法求数组最大值 c语言,使用分治法求最大子数组的下标。
  4. python中与label类似的控件是_python中tkinter的使用(控件整理)(一)
  5. 【Flink】Flink TimeServer 之 timerService().registerProcessingTimeTimer
  6. Linux下的PDF阅读器Foxit
  7. (转)ASP.NET 3.5 企业级开发
  8. 【转载】阿里数据技术大图详解
  9. 用verilog实现数字频率计
  10. php幻灯片图片不显示不出来,织梦dedecms默认模板幻灯片无法显示图片的解决方法...
  11. AMD、ARM、Intel、Qualcomm
  12. 基于java的农村养老保险系统 ssh框架
  13. CSP-S 2022游记
  14. 吴忠文化旅游的现状与问题
  15. 重学Mysql之Mysql8.0修改密码策略
  16. NFT 的价值与法律风险
  17. HWiNFO32无法加载
  18. 自动驾驶技术基础——千寻定位
  19. C#单个去水印软件 - 解决方案
  20. python小程序之七段数码管的绘制

热门文章

  1. RNN代码简单实现(周杰伦歌词示例)
  2. 树的遍历(Java)
  3. bash(CVE-2014-6271) shellshock-破壳漏洞复现
  4. 【Hexo】nexT主题使用攻略基础——添加分类、标签及关于
  5. IDEA注释方式快捷键
  6. 20230103编译ToyBrick的TB-RK3588X的Andorid12的LOG02
  7. bp神经网络需要多少样本,bp神经网络训练时间
  8. 程序员去外包公司待遇怎么样?外包薪资高吗?
  9. mysql 命令行登录详解
  10. 致创业新人,我网络创业的一些心得。