利用SVG的<use>和CSS自定义属性实现惊艳动画效果

本文详细介绍了如何通过SVG的symbol和use元素结合CSS自定义属性实现复杂的动画效果,解决了Shadow DOM样式隔离问题,包含图标系统、数据可视化和角色动画等实用案例。

利用SVG的和CSS自定义属性实现惊艳动画效果

SVG是那些既优雅又有时令人恼火的Web技术之一。在这篇文章中,先驱作者和网页设计师Andy Clarke解释了他对隐藏在Shadow DOM中的SVG元素进行动画处理的技术。

我最近解释了我如何使用<symbol><use>和CSS媒体查询来开发我称之为自适应SVG的技术。Symbols让我们可以一次性定义一个元素,然后重复使用它,使SVG动画更易于维护、更高效且更轻量。

自从我写了那个解释之后,我在我的网站上设计并实现了新的"Magnificent 7"动画图形。它们以网页设计先驱为主题,展示了七位杰出的老西部角色。

理解Shadow DOM障碍

当你使用use引用symbol的内容时,浏览器会在Shadow DOM中创建它的副本。每个<use>实例都成为被引用<symbol>的自己的封装副本,这意味着来自外部的CSS无法突破障碍直接样式化任何元素。

例如,在正常情况下,这个tapping值会触发CSS动画:

1
2
3
<g class="outlaw-1-foot tapping">
  <!-- ... -->
</g>
1
2
3
.tapping {
  animation: tapping 1s ease-in-out infinite;
}

但是当相同的动画应用于同一只脚的<use>实例时,什么也不会发生:

1
2
3
4
5
<symbol id="outlaw-1">
  <g class="outlaw-1-foot"><!-- ... --></g>
</symbol>

<use href="#outlaw-1" class="tapping" />
1
2
3
.tapping {
  animation: tapping 1s ease-in-out infinite;
}

这是因为<symbol>元素内的<g>位于受保护的shadow tree中,而CSS Cascade在<use>边界处完全停止。这种行为可能令人沮丧,但它是故意的,因为它确保重用的symbol内容保持一致和可预测。

CSS自定义属性来救援

在研究我的先驱动画时,我了解到常规的CSS值无法跨越边界进入Shadow DOM,但CSS自定义属性可以。尽管你不能直接样式化<symbol>内的元素,但你可以将自定义属性值传递给它们。

我在应用于<symbol>内容的内联样式中添加了rotate:

1
2
3
4
5
6
7
8
<symbol id="outlaw-1">
  <g class="outlaw-1-foot" style="
    transform-origin: bottom right; 
    transform-box: fill-box; 
    transform: rotate(var(--foot-rotate));">
    <!-- ... -->
  </g>
</symbol>

然后,定义了脚部敲击动画并将其应用于元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@keyframes tapping {
  0%, 60%, 100% { --foot-rotate: 0deg; }
  20% { --foot-rotate: -5deg; }
  40% { --foot-rotate: 2deg; }
}

use[data-outlaw="1"] {
  --foot-rotate: 0deg;
  animation: tapping 1s ease-in-out infinite;
}

向Symbol传递多个值

一旦我设置了一个symbol来使用CSS自定义属性,我就可以向任何<use>实例传递任意多的值。例如,我可能为fill、opacity或transform定义变量。优雅的是,每个<symbol>实例都可以有自己的一组值。

1
2
3
4
5
6
7
<g class="eyelids" style="
  fill: var(--eyelids-colour, #f7bea1);
  opacity: var(--eyelids-opacity, 1);
  transform: var(--eyelids-scale, 0);"
>
  <!-- etc. -->
</g>
1
2
3
4
5
6
7
8
9
use[data-outlaw="1"] {
  --eyelids-colour: #f7bea1; 
  --eyelids-opacity: 1;
}

use[data-outlaw="2"] {
  --eyelids-colour: #ba7e5e; 
  --eyelids-opacity: 0;
}

多色图标系统

当我需要维护一组图标时,我可以在<symbol>内定义一个图标,然后使用自定义属性来应用颜色和效果。不需要为每个主题复制SVG,每个use都可以携带自己的值。

例如,我为这个Bluesky图标中的<path>的默认填充颜色应用了一个--icon-fill自定义属性:

1
2
3
<symbol id="icon-bluesky">
  <path fill="var(--icon-fill, currentColor)" d="..." />
</symbol>

然后,每当我需要改变该图标的外观时——例如,在<header><footer>中——我可以向每个实例传递新的填充颜色值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<header>
  <svg xmlns="http://www.w3.org/2000/svg">
    <use href="#icon-bluesky" style="--icon-fill: #2d373b;" />
  </svg>
</header>

<footer>
  <svg xmlns="http://www.w3.org/2000/svg">
    <use href="#icon-bluesky" style="--icon-fill: #590d1a;" />
  </svg>
</footer>

这些图标形状相同,但由于它们的内联样式而看起来不同。

使用CSS自定义属性的数据可视化

我们可以在更多实际方式中使用<symbol><use>。它们也有助于创建轻量级的数据可视化,所以想象一个关于三位著名西部警长的信息图:Wyatt Earp、Pat Garrett和Bat Masterson。

每位警长的个人资料使用相同的SVG三个symbols:一个代表警长职业生涯长度的条形图,另一个代表逮捕次数,还有一个代表击杀次数。通过向每个<use>实例传递自定义属性值,可以改变条形长度、逮捕比例和击杀颜色,而无需复制SVG。

我首先为这些项目创建了symbols:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
  <symbol id="career-bar">
    <rect
      height="10"
      width="var(--career-length, 100)" 
      fill="var(--career-colour, #f7bea1)"
    />
  </symbol>
  
  <symbol id="arrests-badge">
    <path 
      fill="var(--arrest-color, #d0985f)" 
      transform="scale(var(--arrest-scale, 1))"
    />
  </symbol>
  
  <symbol id="kills-icon">
    <path fill="var(--kill-colour, #769099)" />
  </symbol>
</svg>

每个symbol接受一个或多个值:

  • --career-length调整职业条形的宽度
  • --career-colour改变该条形的填充颜色
  • --arrest-scale控制逮捕徽章的大小
  • --kill-colour定义击杀图标的填充颜色

环境动画

我在为网站的"Magnificent 7"创建动画图形时开始学习在symbols内动画化元素。为了降低复杂性并使我的代码更轻量、更易于维护,我需要定义每个角色一次并在多个SVG中重复使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- Symbols library -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
  <symbol id="outlaw-1">[…]</symbol>
  <!-- ... -->
</svg>

<!-- Large screens -->
<svg xmlns="http://www.w3.org/2000/svg" id="svg-large">
  <use href="outlaw-1" />
  <!-- ... -->
</svg>

<!-- Small screens -->
<svg xmlns="http://www.w3.org/2000/svg" id="svg-small">
  <use href="outlaw-1" />
  <!-- ... -->
</svg>

但我不希望这些角色保持静态;我需要微妙的动作让它们栩栩如生。我希望他们的眼睛眨眼、脚部敲击、胡须抖动。

眨眼效果

我通过在歹徒眼睛上放置一个SVG组,然后改变其不透明度来实现眨眼效果。

为了使其成为可能,我向该组添加了一个带有CSS自定义属性的内联样式:

1
2
3
4
5
<symbol id="outlaw-1" viewBox="0 0 712 2552">
  <g class="eyelids" style="opacity: var(--eyelids-opacity, 1);">
    <!-- ... -->
  </g>
</symbol>

然后,我通过改变--eyelids-opacity来定义眨眼动画:

1
2
3
4
5
6
@keyframes blink {
  0%, 92% { --eyelids-opacity: 0; }
  93%, 94% { --eyelids-opacity: 1; }
  95%, 97% { --eyelids-opacity: 0.1; }
  98%, 100% { --eyelids-opacity: 0; }
}

…并将其应用于每个角色:

1
2
3
4
5
use[data-outlaw] {
  --blink-duration: 4s;
  --eyelids-opacity: 1;
  animation: blink var(--blink-duration) infinite var(--blink-delay);
}

…这样每个角色不会同时眨眼,我通过传递另一个自定义属性设置了不同的--blink-delay

1
2
3
use[data-outlaw="1"] { --blink-delay: 1s; }
use[data-outlaw="2"] { --blink-delay: 2s; }
use[data-outlaw="7"] { --blink-delay: 3s; }

脚部敲击效果

一些角色会敲击他们的脚,所以我也向那些组添加了带有CSS自定义属性的内联样式:

1
2
3
4
5
6
7
<symbol id="outlaw-1" viewBox="0 0 712 2552">
  <g class="outlaw-1-foot" style="
    transform-origin: bottom right; 
    transform-box: fill-box; 
    transform: rotate(var(--foot-rotate));">
  </g>
</symbol>

定义脚部敲击动画:

1
2
3
4
5
@keyframes tapping {
  0%, 60%, 100% { --foot-rotate: 0deg; }
  20% { --foot-rotate: -5deg; }
  40% { --foot-rotate: 2deg; }
}

并将这些额外的自定义属性添加到角色的声明中:

1
2
3
4
5
6
7
8
use[data-outlaw] {
  --blink-duration: 4s;
  --eyelids-opacity: 1;
  --foot-rotate: 0deg;
  animation: 
    blink var(--blink-duration) infinite var(--blink-delay),
    tapping 1s ease-in-out infinite;
}

抖动效果

最后通过带有CSS自定义属性的内联样式使角色的胡须抖动,该属性描述了他的小胡子如何变换:

1
2
3
4
5
6
7
<symbol id="outlaw-1" viewBox="0 0 712 2552">
  <g class="outlaw-1-tashe" style="
    transform: translateX(var(--jiggle-x, 0px));"
  >
    <!-- ... -->
  </g>
</symbol>

定义抖动动画:

1
2
3
4
5
6
7
@keyframes jiggle {
  0%, 100% { --jiggle-x: 0px; }
  20% { --jiggle-x: -3px; }
  40% { --jiggle-x: 2px; }
  60% { --jiggle-x: -1px; }
  80% { --jiggle-x: 4px; }
}

并将这些属性添加到角色的声明中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use[data-outlaw] {
  --blink-duration: 4s;
  --eyelids-opacity: 1;
  --foot-rotate: 0deg;
  --jiggle-x: 0px;
  animation: 
    blink var(--blink-duration) infinite var(--blink-delay),
    jiggle 1s ease-in-out infinite,
    tapping 1s ease-in-out infinite;
}

有了这些运动部件,角色变得栩栩如生,但我的标记仍然非常精简。通过将几个动画组合到一个声明中,我可以在不向SVG添加更多元素的情况下编排它们的动作。每个歹徒共享相同的基础<symbol>,它们的个性完全来自CSS自定义属性。

陷阱和解决方案

尽管这种技术似乎无懈可击,但最好避免一些陷阱:

  • CSS自定义属性只有在<symbol>内使用var()引用时才起作用。忘记这一点,你会想知道为什么没有更新。
  • 不自然继承的属性,如fill或transform,需要在它们的值中使用var()才能从级联中受益。
  • 最好在自定义属性旁边包含一个回退值,如opacity: var(--eyelids-opacity, 1);,以确保即使没有应用自定义属性值,SVG元素也能正确渲染。
  • 通过style属性设置的内联样式具有优先权,所以如果你混合使用内联和外部CSS,请记住自定义属性遵循正常的级联规则。
  • 你始终可以使用DevTools检查自定义属性值。选择一个<use>实例并检查Computed Styles面板,查看哪些自定义属性处于活动状态。

结论

<symbol><use>元素是SVG中最优雅但有时令人沮丧的方面之一。Shadow DOM障碍使它们动画化更加棘手,但CSS自定义属性充当了桥梁。它们让你可以跨越那个无形的边界传递颜色、运动和个性,从而产生更清晰、更轻量,最重要的是,更有趣的动画。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计