简简单单实现画笔工具,轻松绘制丝滑曲线

864 阅读6分钟

大家好,我是前端西瓜哥。

最近照着 Figma 做了个简单的画笔功能,实现起来还是比较简单的。

图片

我正在开发的 suika 图形编辑器:

github.com/F-star/suik…

线上体验:

blog.fstars.wang/app/suika/

绘制流程

首先是监听按下鼠标,我们记录好此时鼠标的位置,作为路径的起点,并记录此时是 “拖拽状态”。

然后按住鼠标不放,进行拖拽。

我们监听鼠标移动事件,如果是 “拖拽状态”,我们通过鼠标事件拿到最新的鼠标位置,保存起来。

鼠标移动事件会在鼠标移动时按较小的间隔不断触发,于是我们能拿到一个个的点。

我们将这些点按顺序连起来,然后渲染到画布上,这样就在画布上绘制出了线条。

图片

最后鼠标释放,这条线段就正式被绘制出来了,我们退出 “拖拽状态”,并把新增一个路径对象的数据添加到历史记录。

对离散点做曲线拟合

我们是无法从浏览器的 API 拿到曲线的,能拿到的只是一堆的点。

浏览器会在鼠标移动时按照特定的频率触发鼠标事件。

移动得慢,会拿到密集的点,移动得快,就会拿到稀疏的点。

它的采样频率比较适中,如果希望提高采样率,单位时间内捕获更多的点,但那是不可能的,因为浏览器做了限制。

如果高采样率很重要,可以考虑做桌面应用。

但不管如何,最后我们可以拿到一条折线,但和我们真实世界中用画笔绘制出的光滑线条有很大出入。

所以这里需要对离散的采样点做光滑化处理,最终转换为点更少的曲线表达。

这种操作称为 曲线拟合(Curve Fitting)

算法

这里我就想到了 paper.js 的 path.simplify(tolerance)。该方法的作用就是曲线拟合,将一个复杂的 path 简化为数据量更少形状更平滑的 path。

tolerance 是光滑程度,越大就越光滑,但同时也越不像原来的路径形状。

它使用的是一种叫做 Schneider algorithm 的曲线拟合算法,并在其上做了一些改进。

该算法的原理不是本文讨论的重点,感兴趣的可以去找一篇发布于 1990 年,名为《An Algorithm for Automatically Fitting Digitized Curves》的文章,并收录在一本名为《Graphics Gems》的书中。

关注公众号,回复 ”曲线拟合“,获取《Graphics Gems》电子书

paper.js 的方法很好,但它的这个算法是和 paper.js 对象耦合在一起的,我不好抽出来,有一些工作量。

最后我找到一个 fit-curve 库,正是基于 Schneider algorithm 的实现。

github.com/soswow/fit-…

其用法为:

import fitCurve from 'fit-curve';

const points = [[00], [1010], [100], [200]]; // 需要处理的有序点集
const error = 50// error 越大,曲线越光滑

const bezierCurves = fitCurve(points, error);
// bezierCurves[0] 为 [[0, 0], [20.27317402, 20.27317402], [-1.24665147, 0], [20, 0]]
// 代表的是三阶贝塞尔曲线:[起点, 控制点1,控制点2, 终点]

然后我们在鼠标释放的时候,对折线线条应用该算法,就能得到一个平滑的曲线。

这里我给 error 设置非常小的值,让曲线更接近原来的形状,同时也能有效减少点的数量。

图片

曲线拟合算法还有其它的实现,比如 RDP algorithm,读者可以都尝试一下,看看哪个效果更好。

更进阶的,可以像 paper.js 一样尝试去改进算法,甚至融合创造新的算法。

其它

这里的画笔工具,思路是在绘制折线后做一个曲线拟合,将线条做平滑处理。

还有一种做法是在绘制过程中就进行曲线拟合(也叫防抖),甚至可以引入压感动态改变线的局部粗细,这样更接近像是 Photoshop 这类基于位图的画笔工具形态。

结尾

我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。


相关阅读,

贝塞尔曲线是什么?如何用 Canvas 绘制三阶贝塞尔曲线?

平面几何:判断点是否在多边形内(射线法)

给定一个边与边可能相交的多边形,求它的轮廓线

图形编辑器开发:钢笔工具功能说明书

图形编辑器开发:钢笔工具的实现