大家好,我是前端西瓜哥。
最近照着 Figma 做了个简单的画笔功能,实现起来还是比较简单的。
我正在开发的 suika 图形编辑器: https://github.com/F-star/suika 线上体验: https://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 的实现。
https://github.com/soswow/fit-curve
其用法为:
import fitCurve from 'fit-curve';
const points = [[0, 0], [10, 10], [10, 0], [20, 0]]; // 需要处理的有序点集
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 这类基于位图的画笔工具形态。
我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。