用 JavaScript 绘制 SVG 绳索

本文将带大家了解我将 SVG 路径转化为矢量绳索图的过程。

我们将学习如何把左边的路径变成右边的绳索:

我的同事在项目中遇到这个问题,让我印象深刻,于是我一有空闲就尝试,并从中获得不少乐趣。

所以,我决定写一篇文章,与大家分享整个过程。

本文不是编码教程,只概述了我的操作步骤,但本文的所有代码都可以使用。

大家可以跳过前半部分直接查看后面的交互式 demoCodePen 代码。但我强烈建议大家阅读整篇文章。


理念

根据绳索的特写照片,我们可以确定这条绳索由多股绳子相互缠绕而成。绳索在视觉上被分成了几段,每段的 2D 投影类似一个扭曲的多边形。

我们的目标是用 JavaScript 创建这些多边形。

首先,我们要生成一些简单的矩形多边形,然后对矩形多边形进行微调,让矩形多边形看起来像绳索。


如何解决这个问题?

把绳索画出来非常简单,但把绳索转换成代码可不容易。

许多初级开发人员遇到这种问题都会很苦恼。他们通常会立即开始编码,但随即陷入困境,所以解决这个问题至关重要。

我经常对我的学生们说,编程是为了解决问题,而代码只是解决问题的工具。

我是一个视觉思考者,解决问题时喜欢在纸上画出来。大家也可以在电脑附近放上纸和笔,在敲代码之前先在纸上画出来。

在本子上一顿涂鸦之后,我获得了下列图像,其中左页右下方的图像我最满意。

这个图像并不完美,但容易编码,所以我选择它,作为编码的起点。


过程

从 SVG 路径开始

我们的目标是制作一个将 SVG 路径变成绳索的程序。该程序需支持直线段(多边形)和贝塞尔曲线。

首先,创建一个简单的弯曲路径:

<path
  d="M 50 150
     C 150 150, 150  50, 250  50
     C 350  50, 350 150, 450 150"
/>

将该路径分成相等的几部分

我们可以把路径分成几部分,把每部分当作一个绳段。在这个项目中,我们把路径以 n 个像素为单位均分为几个相等的部分。

在切分路径前,我们需要获取路径的总长度,便于确定何时停止迭代,以及获得特定长度的点的函数。

还好浏览器为我们提供了方法:

getTotalLength 返回路径的长度;

getPointAtLength 返回路径上特定距离的点。

我们不需要在页面上渲染路径,这两种方法只在内存中的路径下工作。

下面计算这些点的函数:

function getPathPoints(d, step = 10) {
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  path.setAttribute("d", d);

  const length = path.getTotalLength();

  const count = length / step;

  const points = [];

  for (let i = 0; i < count + 1; i++) {
    const n = i * step;
    points.push(path.getPointAtLength(n));
  }

  return points;
}

大家可能会注意到图片中开始和结束处有两个额外的点,它们并不在上面的代码片段中。我们暂时先不管它们,下文会用到这两个点。

关于服务器端渲染的说明:
这些方法不能用在服务器上,但我检查了一些服务器端语言,几乎所有语言都有这两种方法的变体。比如你可以在 NodeJS 上使用 svg-path-properties 库。


增加厚度

路径切割好之后,我们需要给每个片段增加一些厚度。我们将画一条穿过每个点的法线,通过法线来增加厚度。

我们这个用例中的法线的数值不需要太精确,近似值就可以。在曲线上画近似法线有一个简单的方法,需要三个连续的点:

上图的法线穿过 P 点,是 PpPPn 三个点组成的 α 角的平分线。点 PpPn 是辅助点。我们使用法线所在点的前后两个点作为辅助点。

这就是在上一步中添加了额外两个点的原因。添加的两点是为了确保第一个点前面和最后一个点的后面都有点。

我有代码,因为我的 Vertigo 项目解决过同样的问题。


将法线连接成片段

这一步很简单,只需连接相邻的法线,形成块状片段,尽量把块状片段的角度画圆一些。


让片段的角变圆润

我们使用 Chaikin 的方法把角画圆,Chaikin 是一种生成曲线的递归细分算法。该算法以多边形的每条线为基础,以特定的比例(通常是 0.25)在组成多边形的两侧边线找到两个点,然后用这两个新创建的点替换原来的点,递归地重复整个过程,直到获得我们满意的结果。

这个操作听起来很复杂,大家可以查看下面的互动示例:

这个让角度变圆的方法并不返回贝塞尔曲线,而是返回一个多边形。这在多数情况下是好的,因为多边形比贝塞尔曲线更容易进行几何操作。只要有足够的迭代,人眼无法分辨。


倾斜片段

绳索是由多条线缠绕在一起形成的。为模仿这种缠绕感,我们要把片段倾斜,这可以通过把平分线旋转一个固定角度来快速实现。


最后

如果去掉辅助元素,把整个图片变细,就非常像一条绳索。大家可以根据自己的需求选择是否继续。

如果与绳索的特写进行对比,可以发现我们制作的多边形与真实的绳索还是有区别的,我们的四边形非常规律,但绳索的片段有重叠的阴影。

因此,我们可以在进行一些改进,让我们的四边形片段更像真实的绳索。


改进

回到最开始的草图。这次我们先不倾斜分段。

切掉草图中的两个角,增加 3 和 8 两个角。

此时的片段效果太过块状和数学化。

不过,我没有保留代码,所以没法给大家看示例。但我知道这个方向是正确的。接下来我通过移动点来进行微调,大家可以在上面的图片中看到效果,我认为现在看起来更加自然圆滑。

请注意第一个和最后一个片段略有不同,因为它们两个不处于两个片段之间,因此它们在代码中的处理方式与所有其他片段不同。


让片段的角变圆润

如果我们将 Chaikin 的算法应用于新片段(闭合的四边形), 会得到下图:

每个四边形都很完美很圆润,但有些奇怪,而且失去了缠绕的视觉效果。如果我们保留两个尖角,效果会更好。

为了保持边角,我们把片段线分成两条线,然后分别对每条线应用圆角算法,这会产生更好的视觉效果,仿佛每条线互相覆盖。

修复缝隙(可选)

圆角步骤之后会出现一个非常小但是很明显的问题,因为这个过程删除了一些点,所以出现了小缝隙。

我不介意这个问题,因为这些缝隙只在线条非常细的情况下才能看到,线条粗点就能遮盖这个问题。

但我修复了它,只是想挑战一下自己。有一项黑客技术可以避免在使用 Chaikin 算法时避免删除点——将那个点变成三个点。这样就可以创造两条宽度为零的边。无论我们做多少次递归细分,零的比率仍然是零。

然而,这项黑客技术有一个缺点。它会在每次迭代中重复这三个点。因此,如果使用了这个修复方法,需要清理重复的点。

如上所述,我不介意这个问题,所以我下面的步骤中禁用了这个修复方法。


倾斜片段

再次倾斜所有片段,与上一步一样,只将平分线旋转一个固定的角度。


添加颜色

去掉辅助元素并添加一些颜色,现在看起来真的很像一条绳索了,但我们还有最后一步。


把它做成动画

我们可以将绳索做成动画。把它制作成动画便于形成图表。

我们采用简单直接的方法。在每一帧中更新路径,重新生成绳索并重新渲染。我们需要一个动画循环和一个更新路径的方法。如果你对动画循环不熟悉,可以查看这篇文章

为了移动路径,我写了一个函数来更新路径上每个点的 y 坐标:

function getStepPath() {
  const y = easing(t) * 100 + 50;
  const y1 = y;
  const y2 = 200 - y;

  return `M  50 ${y2}
          C 150 ${y2}, 150 ${y1}, 250 ${y1}
          C 350 ${y1}, 350 ${y2}, 450 ${y2}`;
}

t 是随着时间推移在 0 和 1 之间跳动的值。然后我们可以对 t 值应用缓和,并计算出所有的 y 值。

为让绳索实现动画效果,我们需要把这个逻辑插入动画循环中。

这里我就不再深入探讨这个问题了。感兴趣的话可以查看下列代码试一下。

绳索终于创建完成了!感谢大家的阅读!


总结

这篇文章的写作时间比我预期的要长,希望大家喜欢!制作交互式范例相当耗时,但很有成就感,也收获了很多快乐。

欢迎大家:

• 查看互动演示

• 查看 CodePen 上的代码

• 挖掘此页面使用的互动例子(代码有点乱)。



原文作者:Stanko
原文链接:https://muffinman.io/blog/draw-svg-rope-using-javascript
推荐阅读
相关专栏
开发者实践
182 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。