共计 2081 个字符,预计需要花费 6 分钟才能阅读完成。
动机
在公司的某次周会上,我吐槽了某产品中一个显示鼠标轨迹的成果实现得比拟形象:
能够看到它的实现形式是将 mousemove
事件触发时的坐标,用长宽不一的矩形连接起来,所以连接处呈现了显著的“断裂”,整个轨迹也不平滑,而且其宽度和透明度的“突变”也比拟僵硬,有显著断层。
而我现实中的鼠标轨迹应该是长这样的:
整个轨迹是一条绝对平滑的曲线,两头不应该有僵硬的“断裂”,而且轨迹的宽度和透明度都平均变动。
过后我感觉这么简略一个成果齐全应该做得欠缺一点,也花不了多少工夫。
然而,一个周末的中午,我正在洗碗,忽然脑子里灵光一闪,我意识到,在 web canvas 上要实现一个「完满」的鼠标轨迹成果仿佛并没有设想的那么简略。于是我决定本人尝试一下,就有了这个我的项目。
问题
所谓「并没有设想的那么简略」次要是要解决这几个问题:
- 通过
mousemove
事件获取的鼠标轨迹是离散的坐标点,而不是实在的轨迹曲线,如何通过离散坐标绘制平滑曲线? - 鼠标轨迹的透明度应该是突变的,web canvas 上并没有提供在一个 path 上做线性突变的接口,这个成果如何实现?
- 鼠标轨迹的粗细也应该是突变的,web canvas 上的繁多 path 也没有提供画笔粗细突变的接口,这个成果又如何实现?
计划
如何通过离散坐标绘制平滑曲线?
如果你用过 Photoshop 中的钢笔工具,答案其实就很简略,用贝塞尔曲线。Photoshop 中的钢笔工具其实就是一个贝塞尔曲线编辑器,通过终点、起点以及两个控制点,就能够在终点和起点间建设一条曲线。
而如果一个两头点上的两个控制点满足肯定的法则,就能够实现曲线的间断,也就是视觉效果上的平滑。感兴趣的话能够浏览「用钢笔工具绘图」中的内容。
那么两头点上的两个控制点满足什么样的法则就能够实现曲线的间断呢?其实也很简略,就是两头点和两个控制点在同一直线上即可。
如下图,鼠标通过 A、B、C 三点,此时 B 点和他的两个控制点 C1 和 C2 在同一直线上,整个曲线在 B 点处就是平滑的。其数学逻辑也很简略,三点处于同一直线就意味着 B 点在 C1 方向和 C2 方向上的斜率都雷同,这样曲线就平滑了。
那么,在已知 A、B、C 三点坐标的状况下如何计算出每个点的控制点呢?一个简略的方法如下如所示:
- 计算角 p1-pt-p2 的角平分线,以及此角平分线通过点 pt 的垂线 c1-pt-c2
- 取 p1、p2 在 c1-pt-c2 上的投影点中距离 pt 点较近的点 c2
- 在 c1-pt-c2 上取与 c2 点绝对 pt 对称的点 c1
此时用计算出的 c1、c2 点作为 pt 点的控制点,就能生成一个成果不错的平滑曲线了,同时 c1、c2 到 pt 点的间隔还能够用一个 tension 参数进行调节,从而达到调节曲线平滑水平的作用。
如何在曲线上实现宽度的突变?
首先,CanvasRenderingContext2D
这套 API 并没有提供描边门路时突变笔刷宽度的接口,也就是说,如果仅仅用 bezierCurveTo
和 stroke
这两个接口是没有方法实现像文章开始时形容的那种「完满」的鼠标轨迹的。
解决这个问题的其中一个方法,就是把门路变为形态。简略来说,就是把一段有宽度的贝塞尔曲线,看做是由两条曲线和两条直线所围成的图形:
两头彩色的曲线用一个有宽度的画笔描边之后,其实和红色区域填充之后的成果是一样的,这就是所谓把门路变为形态。这样一来,咱们依据须要来调整红色线框的形态,就能够实现一个看起来画笔宽度突变的曲线了,至于如何计算这个线框这里先按下不表。
如何在曲线上实现透明度的突变?
同样的,CanvasRenderingContext2D
这套 API 也没有提供描边门路或填充区域时突变笔刷透明度的接口。这时就不得不应用「宰割」法来模仿一个突变成果了。也就是说,如果有一段曲线在绘制时须要将画笔透明图从 1 变为 0,咱们就把这条曲线宰割成 100 个曲线片段顺次绘制,并且绘制这些片段时所用的透明度逐步变动,这样就能够在视觉上实现透明度突变的成果了。
如上图所示,咱们能够在一条贝塞尔曲线上计算出若干个点,用这些点把这条曲线宰割成多条曲线,而后给与每条曲线不同的透明度,这样在视觉上就能够实现相似透明度突变的成果。
但仔细的同学必定会发现一个问题,上图中宰割点之间的间隔是不一样的,这里又波及到一个概念:匀速贝塞尔曲线。三次贝塞尔曲线的公式如下:
所以如果咱们让输出,也就是 t 在 [0, 1] 上匀速变动,失去的值则不是匀速的,也就是上图中空心圆点的间隔是不同的。然而,要计算出平均宰割贝塞尔曲线的点十分麻烦,往往须要迭代计算能力求得一个近似值。
然而,就算用简略的宰割办法,只有宰割的数量够多,比方宰割成 50 段,人眼也基本上看不出来透明度的变动是不平均的,所以理论应用场景中没有必要肯定要算出平均宰割的点。
另外,宰割法事实上也同样能够解决下面宽度突变的问题,把曲线宰割成若干段,给与每一段不同的线宽,曲线的宽度看起来就是平均变动的了,而且这种方法事实上比下面讲的计算曲线边框的方法速度更快。
总结
相干代码我曾经封装成了一个 npm 包:laser-pen,欢送 start、issue、pr
没事多洗洗碗。说不定就会有意外的播种 😂