使用 SVG 动画书法

Avatar of Claus Colloseus
Claus Colloseus

DigitalOcean 为您的旅程各个阶段提供云产品。 立即开始使用 200 美元的免费积分!

在 Stackoverflow 上,偶尔会有人问到是否有一种与 stroke-dashoffset 技术 等效的方法来为 SVG stroke 属性进行动画,这种方法适用于 fill 属性。 但仔细观察后,人们真正想问的问题其实是这样的

我有一个东西,它有点像一条线,但因为它有不同的笔刷宽度,所以在 SVG 中它被定义为路径的填充。

如何为这个“笔刷”制作动画?

简而言之:**如何为书法制作动画?**

一个遮罩路径覆盖了书法笔刷

这种技术的原理很简单:在书法之上绘制一条第二个(平滑的)路径,使其沿着笔刷线移动,然后选择合适的笔刷宽度,使其覆盖所有书法。

这条上面的路径将用作下面路径的遮罩。 将 stroke-dashoffset 动画技术应用于遮罩路径。 结果将看起来像是底部的路径正在实时地“写入”屏幕上。

这是一个使用 mask 的情况,而不是 clip-path - 后者无法实现。 剪切路径始终引用路径的填充区域,但忽略 stroke

最简单的变体是将遮罩路径中的路径的 stroke: white 设置为白色。 然后,任何不在白色区域之外的东西都会被隐藏,而任何在里面的东西都将显示出来,没有任何改变。

查看 CodePen 上由 ccprog (@ccprog) 创建的 书法动画:基本示例

到目前为止,一切都很简单。 但是,当书法线重叠时,事情就变得棘手了。 这就是在朴素的实现中发生的情况

查看 CodePen 上由 ccprog (@ccprog) 创建的 书法动画:错误的交叉

在交叉点,遮罩会显示部分交叉笔刷。 因此,书法必须被切割成不重叠的部分。 按绘制顺序堆叠它们,并为每个部分定义单独的遮罩路径。

遮罩路径和书法笔刷的切割必须匹配

最棘手的部分是保持绘制是单个连续笔画的印象。 如果你切割一条平滑的路径,只要两条路径的切线在公共点具有相同方向,端点就会匹配。 笔画端点垂直于该方向,并且切书法线的切割必须完全对齐这一点至关重要。 注意所有路径具有连续的方向。 按顺序为它们制作动画。

虽然许多线动画可以使用 stroke-dasharray 长度的粗略数学运算,但这种情况需要精确的测量(尽管的舍入不会造成伤害)。 作为提醒,你可以在 DevTools 控制台中获取它们

document.querySelector('#mask1 path').getTotalLength()

查看 CodePen 上由 ccprog (@ccprog) 创建的 书法动画:划分交叉点

“按顺序”部分在 CSS 中编写起来有点笨拙。 最好的模式可能是为所有部分动画设置相同的开始时间和总时长,然后在 stroke-dashoffset 变化之间设置中间关键帧。

类似于这样

@keyframes brush1 {
  0% { stroke-dashoffset: 160; } /* leave static */
  12% { stroke-dashoffset: 160; } /* start of first brush */
  44% { stroke-dashoffset: 0; }   /* end of first brush equals start of second */
  100% { stroke-dashoffset: 0; }   /* leave static */
}

@keyframes brush2 {
  0% { stroke-dashoffset: 210; } /* leave static */
  44% { stroke-dashoffset: 210; } /* start of second brush equals end of first */
  86% { stroke-dashoffset: 0; }   /* end of second brush */
  100% { stroke-dashoffset: 0; }   /* leave static */
}

接下来,你将看到 SMIL 动画如何提供一种更流畅和更具表现力的方式来定义时间。 如果坚持使用 CSS,使用 Sass 完成的计算可能非常有用,因为它可以处理一些数学运算。

遮罩路径(左)及其应用(右)

如果遮罩路径的曲线半径小于笔刷宽度,就会出现类似的问题。 当动画穿过该曲线时,可能会发生中间状态看起来严重弯曲。

解决方案是将遮罩路径移出书法曲线。 你只需要确保它的内边缘仍然覆盖笔刷。

你甚至可以切割遮罩路径并错开端点,只要切割边缘匹配即可。

半径保持足够大

查看 CodePen 上由 ccprog (@ccprog) 创建的 书法动画:划分交叉点

因此,你甚至可以绘制一些复杂的东西,例如这个例子中的阿拉伯书法

查看 CodePen 上由 ccprog (@ccprog) 创建的 马哈茂德二世图章 - 文本动画

原始设计,即奥斯曼苏丹 马哈茂德二世图章,由一位不知名的 19 世纪书法家创作。 矢量版本由维基百科插画家 Baba66 完成。 动画是我试图可视化阿拉伯字母在绘制中的位置。 它基于 Baba66 的早期版本。 知识共享署名-相同方式共享 2.5

以下代码片段展示了用于按顺序和可重复的方式运行动画的进阶方法。

mask path {
  fill: none;
  stroke: white;
  stroke-width: 16;
}

.brush {
  fill: #0d33f2;
}
<mask id="mask1" maskUnits="userSpaceOnUse">
  <path stroke-dasharray="160 160" stroke-dashoffset="160" d="...">
    <!-- animation begins after document starts and repeats with a click
         on the "repeat" button -->
    <animate id="animate1" attributeName="stroke-dashoffset"
             from="160" to="0" begin="1s;repeat.click" dur="1.6s" />
  </path>
</mask>
<mask id="mask2" maskUnits="userSpaceOnUse">
  <path stroke-dasharray="350 350" stroke-dashoffset="350" d="...">
    <!-- animation begins at the end of the previous one -->
    <animate id="animate2" attributeName="stroke-dashoffset"
             from="350" to="0" begin="animate1.end" dur="3.5s" />
  </path>
</mask>
<!-- more masks... -->
<mask id="mask15" maskUnits="userSpaceOnUse">
  <path stroke-dasharray="230 230" stroke-dashoffset="230" d="...">
    <!-- insert an artificial pause between the animations, as if the
         brush had been lifted -->
    <animate id="animate15" attributeName="stroke-dashoffset"
             from="230" to="0" begin="animate14.end+0.5s" dur="2.3s" />
  </path>
</mask>

<g class="brush">
  <path id="brush1" d="...">
    <!-- The mask is only applied  after document starts/repeats and until
         the animation has run. This makes sure the brushes are visible in
         renderers that do not support SMIL -->
    <set attributeName="mask" to="url(#mask1)"
         begin="0s;repeat.click" end="animate1.end;indefinite" />
  </path>
  <path id="brush2" d="...">
    <set attributeName="mask" to="url(#mask2)"
         begin="0s;repeat.click" end="animate2.end;indefinite" />
  </path>
  <!-- more paths... -->
  <path id="brush15" d="...">
    <set attributeName="mask" to="url(#mask2)"
         begin="0s;repeat.click" end="animate15.end;indefinite" />
  </path>
</g>

与我们看过的其他示例不同,此动画使用 SMIL,这意味着它在 Internet Explorer 和 Edge 中不起作用。


这篇文章在 Browser…​unplugged 网站上以德语发布