以RoughJS为例模仿徒手画

RoughJS是一个小的(< 9KB)JavaScript图形库,允许您以粗略的手写样式进行绘制它使您可以使用<canvas>进行绘制SVG在这篇文章中,我想回答有关RoughJS的最流行的问题:它是如何工作的?


一点历史


我对手写的图形,图表和草图的图像着迷,我像一个真正的书呆子一样问自己:我可以在代码的帮助下创建这样的图形,在不影响软件实现的前提下,尽可能精确地手工模拟图形吗?我决定专注于图元(直线,多边形,椭圆和曲线)以创建整个2D图形库。基于此,您可以创建用于绘制图形和图表的库和图形。

在简短地研究了这个问题之后,我发现了Joe Wood及其同事的一篇名为Sketchy rendering的文章,用于信息可视化。其中描述的技术成为该库的基础,尤其是在绘制线条和椭圆形时。

2017年,我编写了该库的第一个版本,该版本仅适用于Canvas。解决了这个问题,我对此失去了兴趣。一年后,我在SVG上工作了很多,并决定使RoughJS适应SVG。我还更改了API的结构以使其更简单,并着重于简单的矢量图形基元。我在Hacker News上谈论了2.0版,突然间它获得了极大的欢迎。在2018年,这是ShowHN第二受欢迎的职位

从那时起,其他人基于RoughJS创建了更多令人惊奇的东西,例如ExcalidrawWhy do Cats and Dogs ...roughViz图形库

现在让我们谈谈算法...

参差不齐


模仿手写图形的基本基础是机会。当我们手工绘制时,任何两个形状都会有所不同。没有人能够完美地精确绘制,因此RoughJS中的每个空间点都针对随机位移进行了调整。随机性的大小由数值参数给出roughness


想象一下A围绕它的一个点和一个圆。现在替换A为该圆内的一个随机点。这个随机性圈的面积由值控制roughness

线数


手写线条从来没有笔直,常常表现出在曲率(形容这里)。我们基于粗糙度随机化线的两个端点。然后,我们再选择两个随机点,这些随机点距离该段的末端大约50%和75%。通过连接曲线的这些点,我们可以获得弯曲的效果


手工绘图时,人们有时会沿直线前后移动铅笔。这对于使线条更亮或者仅校正线条的直线度是必要的。看起来像这样:


为了增加草图效果,RoughJS绘制了两条线。将来,我计划使这方面更具可定制性。

看一下这个画布表面。粗糙度参数更改线条的外观:


在画布上的原始文章中,您可以自己绘画。

手工绘制时,长线通常变得不那么笔直,而更加弯曲。也就是说,产生效果的偏移量的随机性是线长和值的函数randomness但是,缩放此功能不适用于非常长的行。例如,在下图中,使用相同的随机种子绘制同心正方形,即 实际上,它们是一个随机数,但是具有不同的比例。


您可能会注意到,外部正方形的边缘看起来比内部正方形的边缘更加不均匀。因此,我还根据线路长度添加了一个阻尼系数。衰减系数用作各种长度的阶跃函数。


椭圆(和圆圈)


取一张纸并连续不断地尽可能快地画几个圆圈。这是我得到的:


请注意,循环的起点和终点并不总是匹配。RoughJS试图模仿这一点,同时使外观更加完整(该技术改编自giCenter文章)。

该算法找到由n椭圆n的大小确定椭圆点然后,将每个点的值随机化roughness然后通过这些点绘制一条曲线。为了获得断开的端点的效果,从第二个到最后一个的点与第一个点不重合。取而代之的是,曲线连接了第二和第三点。


还绘制了第二个椭圆,以便使循环更闭合并具有附加的草图效果。

在原始文章中,您可以在交互式画布表面上绘制椭圆。改变粗糙度并观察形状如何变化:


在画线的情况下,如果将某些形状缩放到不同大小,则某些假象会变得更加突出。在椭圆形中,此效果更为明显,因为该比例是二次方。在下图中,所有圆都具有相同的形状,但外面的圆看起来更不均匀。


该算法会根据形状的大小自动进行调整,从而增加圆(n)中的点数以下是使用自动调整生成的同一组圆。


填写


虚线 通常用于填充手绘形状在徒手画草图的情况下,线条并不总是保留在形状的轮廓之内。它们也是随机的。密度,角度,线宽均可调节。


上面显示的正方形容易填充,但是在其他形状的情况下,可能会发生各种问题。例如,凹面多边形(角度可能超过180°)通常会导致以下问题:


实际上,上面的图像是从RoughJS早期版本之一的错误报告中获取的。从那时起,我通过适应字符串扫描方法的版本更新了笔划填充算法

字符串扫描算法可用于填充任何多边形。其原理是使用水平线(栅格线)扫描多边形。栅格线从多边形的顶部开始向下。对于每条栅格线,我们确定线与多边形相交的点。我们从左到右建立这些交点。


从点到点,我们从填充模式切换到非填充模式;当栅格线上的每个交点相遇时,就会在状态之间进行切换。在这里,还需要考虑更多,特别是边界情况和扫描优化方法。您可以在此处阅读有关此内容的更多信息:栅格化多边形,或使用伪代码部署扰流器。

字符串扫描算法的执行细节
() .

— (Edge Table, ET), , Ymin. Ymin, Xmin.

— (Active Edge Table, AET), , .

:

interface EdgeTableEntry {
  ymin: number;
  ymax: number;
  x: number; // Initialized to Xmin
  iSlope: number; // Inverse of the slope of the line: 1/m
}

interface ActiveEdgeTableEntry {
  scanlineY: number; // The y value of the scanline
  edge: EdgeTableEntry;
}

, :

1. y y ET. .

2. AET .

3. , AET, ET :

(a) ET y AET , ymin ≤ y.

(b) AET , y = ymax, AET x.

() y, x AET.

(d) y , , .. .

(e) , AET, x y (edge.x = edge.x + edge.iSlope)

在下面的图像中(在互动的原始文章中),每个正方形表示一个像素。您可以移动顶点以更改多边形,并观察传统上将填充哪些像素。


填充笔划时,根据笔划线的给定密度以增量执行栅格线的增加,并使用上述算法绘制每条线。

但是,此算法适用于水平栅格线。为了实现不同的笔触角度,算法首先将形状本身旋转所需的笔触角度。然后计算旋转图形的光栅线。此外,计算出的线沿相反方向旋转回到笔触角度。


不只是填写笔画


RoughJS还支持其他填充样式,但它们均源自相同的填充算法。交叉阴影线包括以一定角度绘制虚线angle,然后以一定角度绘制其他线条angle + 90°之字形试图将一条虚线与上一条虚线连接起来。要获得图案,请沿虚线绘制小圆圈。


曲线


RoughJS中的所有内容均已标准化为曲线-线,多边形,椭圆等。因此,此想法的自然发展是创建草图曲线。在RoughJS中,我们将一组点传递给一条曲线,然后使用曲线近似将它们转换为三次贝塞尔曲线

每个贝塞尔曲线都有两个端点和两个控制点。通过将它们随机化roughness,您可以类似地创建“手写”曲线。


曲线填充


但是,需要逆过程来填充曲线。与其将所有内容归一化为曲线,不如将其归一化为多边形。获取多边形后,可以使用线扫描算法填充曲线形状。

您可以使用三次贝塞尔曲线方程式以所需的频率对曲线上的点进行采样


如果我们使用取决于笔画密度的采样频率,那么我们将获得足够的点来填充图形。但这并不是特别有效。如果曲线的一部分很锐利,那么我们需要更多点。如果曲线的一部分几乎是笔直的,则需要更少的点。一种解决方案是确定曲线曲率/平滑度。如果曲线非常弯曲,则将曲线分为两条较小的曲线。如果平滑,则我们将其简单地视为一条直线。

曲线的光滑度使用中所描述的方法来计算此信息。将平滑度值与公差值进行比较,然后决定是否拆分曲线。

这是公差水平为0.7的同一条曲线:


仅基于公差,该算法即可提供足够的点来表示曲线。但是,它不允许您有效地摆脱可选点。这将有助于第二个参数名为distance为了减少这种方法的点数,使用了Ramer-Douglas-Pecker算法

以下示出了具有距离值的生成的点,等于0.150.751.53.0


根据形状粗糙度,可以设置适当的距离收到多边形的所有顶点后,我们可以精美地填充弯曲的形状:


SVG电路


SVG轮廓是一种非常强大的工具,可用于创建各种令人惊叹的图像,但是正因为如此,很难使用它们。

RoughJS仅通过三个操作即可解析路径并将其标准化:MoveLineCubic Curve。 (path-data-parser)。归一化后,可以使用上述绘制线和曲线的方法来绘制图形。路径上

软件包路径的标准化和曲线点的采样相结合,以计算相应的路径点。

以下是路径的示例点计算M240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100


我喜欢显示的另一个SVG示例是美国的轮廓图:


试试RoughJS


查阅Github上的网站存储库API文档按照 Twitter @RoughLib获取项目信息

All Articles