乐趣区

canvas 绘制双线技巧

楔子
最近一个项目,需要绘制双线的效果,双线效果表示的是轨道(类似铁轨之类的),如下图所示:
负责这块功能开发的小伙,姑且称之为 L 吧,最开始是通过数学计算的方式来实现这种双线,也就是在原来的路径的基础上,计算出两条路径。但是这个过程的计算算挺复杂,而是最终实现的效果很耗性能,性能损耗估计主要在于路径的计算上。
优化技巧
后来他找到我来看这个问题,我在分析了项目背景的情况下,给予了一个简单的绘制技巧,就是先用较粗的线条绘制路径,然后再用较细的线条绘制路径,较细线条的颜色正好是背景颜色。之所以能够使用这个技巧,是因为该项目的绘制背景是纯色的,而不是渐变色或者图片。示例代码如下:
ctx.beginPath();
ctx.fillStyle = ‘blue’;
ctx.rect(10,10,1000,1000);
ctx.fill();

ctx.save();
ctx.strokeStyle = ‘red’;
ctx.lineWidth = 10;
ctx.lineCap = “round”;
ctx.beginPath();
ctx.moveTo(200,100); // 起始点
ctx.lineTo(400,100);
ctx.quadraticCurveTo(500,100,500,200);
ctx.lineTo(500,400);
ctx.quadraticCurveTo(500,500,400,500);
ctx.lineTo(200,500);
ctx.quadraticCurveTo(100,500,100,400);
ctx.lineTo(100,200);
ctx.quadraticCurveTo(100,100,200,100);
ctx.stroke();

ctx.strokeStyle= ‘blue’
ctx.lineWidth = 4;
ctx.stroke();
ctx.restore();

代码的思路是,首先使用纯色 blue 绘制了一个背景,然后使用线条颜色 red 绘制一条线,然后使用较小的线宽,并把线条颜色改成背景颜色 blue,绘制另外一个条线段。最终的绘制效果如下:

到此,项目的这个技术难点问题,算是被解决了。这种解决方法,不仅算法简单,不用构思数学方法来构造双线,而且轻量,不会有性能负担。
背景不是纯色情况
前面说到:之所以能够使用这个技巧,是因为该项目的绘制背景是纯色的,而不是渐变色或者图片。那如果背景是图片或者渐变颜色情况下,用这种技巧,肯定就是失效的了。
之所以会思考这个问题,是得益于公司的技术分享会。我会要求员工定期组织分享会,分享一些经验。在此打个小广告,可以看出我们公司的技术氛围是很好的,所以有兴趣的小伙伴可以抓紧时间投简历。怎么投简历呢,关注微信号 ITman 彪叔。过程中,当时小伙伴 L 也分享了前面提到这种思路。在分享的过程中,我提出了进一步的问题,如果背景不是纯色,而是渐变色或者图片怎么办?并且灵感乍现,想到了一个解决方法,就是使用 ctx.globalCompositeOperation。
有关 globalCompositeOperation 的说明,可以参考如下链接的说明:https://developer.mozilla.org…http://www.w3school.com.cn/ta…
globalCompositeOperation 的定义和用法
globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有)的图像上。其中:

源图像 = 您打算放置到画布上的绘图。
目标图像 = 您已经放置在画布上的绘图

下图显示了 globalCompositeOperation 的不同的值的解释:
要实现双线的绘制,就要求用同样的路径,不同的线宽绘制两条线路(我们称之为目标线路和源线路)。并要达到一条线路抠出另外一条线路的效果。结合上图,我们可以看出 destination-out,source-out,xor 可以达到效果。下面以 destination-out 举例说明。
destination-out 绘制原理说明
比如首先通过 css 设置背景图, 并去掉绘制背景颜色,代码如下:
<body onload=”init()” style=”background: url(../test/images/diffuse.png);”>
然后绘制代码如下:
ctx.save();
ctx.strokeStyle = ‘red’;
ctx.lineWidth = 10;
ctx.lineCap = “round”;
ctx.beginPath();
ctx.moveTo(200,100); // 起始点
ctx.lineTo(400,100);
ctx.quadraticCurveTo(500,100,500,200);
ctx.lineTo(500,400);
ctx.quadraticCurveTo(500,500,400,500);
ctx.lineTo(200,500);
ctx.quadraticCurveTo(100,500,100,400);
ctx.lineTo(100,200);
ctx.quadraticCurveTo(100,100,200,100);
ctx.stroke();

ctx.globalCompositeOperation = ‘destination-out’;
ctx.lineWidth = 4;
ctx.stroke();
ctx.restore();
首先设置路径,然后设置线宽为 10,调用 stroke 方法绘制一条线宽为 10 的路线 A。之后设置 globalCompositeOperation 为 ‘destination-out’,调整线宽为 4,调用 stroke 方法绘制一条线宽为 4 的路线 B。看下 destination-out 的解释:
在源图像外显示目标图像。只有源图像外的目标图像部分会被显示,源图像是透明的。
绘制了线路 A 的 canvas 图像是目标图像,线路 B 是源图像。根据上面解释,只有源图像之外的目标图像能够被显示。最终绘制的效果如下:
xor 和 source-out
把上面的代码的 globalCompositeOperation 修改成 xor,发现效果也是可以的,xor 的解释如下:
使用异或操作对源图像与目标图像进行组合。英文解释如下:Shapes are made transparent where both overlap and drawn normal everywhere else.
意思源和目标的像素重叠 (overlap) 的部分会被变成透明像素,其他部分正常绘制。所以上面示例中,线条 A 和线条 B 重叠的部分会被变成透明。绘制的效果也是线条 A 的被挖空。
对于 source-out,其效果正好和 destination-out 的效果相反:
在目标图像之外显示源图像。只会显示目标图像之外源图像部分,目标图像是透明的。
应此只需要取反操作即可,先用宽度 4 绘制线条 A,然后用宽度 10 绘制线条 B, 其结果也是一样的。
背景不是纯色情况 2
前面的背景是通过 css 的方式设置上去的,如果是通过 canvas 的 drawImage 直接绘制上去,效果就不一样了。还是以 destination-out 为例说明,首先绘制了 image,然后绘制线路 A,此时的目标图像不在是线路 A 组成的图形,而是 image 和线路 A 组合成的图形,此时用 destination-out 的方式绘制线路 B,不仅会挖空线路 A,背景也会被挖空,如下图所示:

应此要想达到真正的双线效果,要么背景只能是用 css 设置,要么用两个 canvas 叠加,一个绘制背景图片,一个绘制路径。当然还有一种方式,就是绘制双线总是在一个临时的 canvas 上面进行,然后把这个临时的 canvas 绘制结果再次绘制到工作 canvas 上面,相关实践留给读者自己进行。
后记
在网络上面搜索 canvas double line,搜索到 stackoverflow 上的一条结果如下:https://stackoverflow.com/que… 其中的答案也是采用了 globalCompositeOperation 设置为 destination-out 的方式。
欢迎关注公众号“ITman 彪叔”。彪叔,拥有 10 多年开发经验,现任公司系统架构师、技术总监、技术培训师、职业规划师。熟悉 Java、JavaScript、Python 语言,熟悉数据库。熟悉 java、nodejs 应用系统架构,大数据高并发、高可用、分布式架构。在计算机图形学、WebGL、前端可视化方面有深入研究。对程序员思维能力训练和培训、程序员职业规划有浓厚兴趣。

退出移动版