利用SVG的
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自定义属性充当了桥梁。它们让你可以跨越那个无形的边界传递颜色、运动和个性,从而产生更清晰、更轻量,最重要的是,更有趣的动画。