构建自适应SVG:使用symbol、use和CSS媒体查询的完整指南

本文详细介绍了如何使用SVG的symbol和use元素结合CSS媒体查询创建自适应SVG图形,实现在不同屏幕尺寸下自动调整布局和动画效果,避免代码重复并提升性能。

构建自适应SVG:使用symbol、use和CSS媒体查询

SVG图形确实可以缩放,但如何让它们更好地适应不同的屏幕尺寸?网页设计先驱Andy Clarke解释了如何使用<symbol><use>和CSS媒体查询构建他称之为“自适应SVG”的技术。

问题背景

假设我设计了一个基于《Bow Wow Bandit》的SVG场景,采用16:9宽高比,viewBox尺寸为1920×1080。这种SVG可以放大和缩小,但在小屏幕上,16:9的宽高比可能不是最佳格式,图像会失去其冲击力。有时,像3:4这样的竖屏方向更适合小屏幕尺寸。

但问题在于,仅使用viewBox很难为不同的屏幕尺寸重新定位内部元素。这是因为在SVG中,内部元素的位置锁定在原始viewBox的坐标系上,因此无法轻松在桌面和移动设备之间更改它们的布局。

解决方案探索

显示和隐藏SVG

最明显的选择是在标记中包含两个不同的SVG,一个用于小屏幕,另一个用于大屏幕,然后使用CSS和媒体查询显示或隐藏它们:

1
2
3
4
5
6
7
<svg id="svg-small" viewBox="0 0 1080 1440">
  <!-- ... -->
</svg>

<svg id="svg-large" viewBox="0 0 1920 1080">
  <!--... -->
</svg>
1
2
3
4
5
6
7
#svg-small { display: block; }
#svg-large { display: none; }

@media (min-width: 64rem) {
  #svg-small { display: none; }
  #svg-mobile { display: block; }
}

但使用这种方法,两个SVG版本都会被加载,当图形复杂时,这意味着下载大量不必要的代码。

使用JavaScript替换SVG

考虑使用JavaScript在指定断点处交换更大的SVG:

1
2
3
4
5
if (window.matchMedia('(min-width: 64rem)').matches) {
  svgContainer.innerHTML = desktopSVG; 
} else {
  svgContainer.innerHTML = mobileSVG;
}

除了JavaScript现在对设计显示至关重要之外,两个SVG通常都会被加载,这会增加DOM复杂性和不必要的重量。此外,维护也成了问题,因为现在有两个版本的图形需要维护。

最终解决方案:一个SVG符号库和多个使用实例

我的目标是:

  • 向小屏幕提供一个版本的Bow Wow Bandit
  • 向大屏幕提供另一个版本
  • 只定义一次图形(DRY原则)
  • 能够调整元素大小和重新定位

使用symbol元素

<symbol>元素允许您定义可重用的SVG元素,这些元素可以隐藏和重用,以提高可维护性并减少代码膨胀。它们就像SVG的组件:创建一次,在需要的地方使用:

1
2
3
4
5
6
7
8
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="quick-draw-body" viewBox="0 0 620 700">
    <g class="quick-draw-body">[…]</g>
  </symbol>
  <!-- ... -->
</svg>

<use href="#quick-draw-body" />

导出独立viewBox

当我创建符号时,需要它有自己的特定viewBox。我将每个元素导出为单独大小的SVG,这给了我将其内容转换为符号所需的尺寸。

以Quick Draw McGraw的帽子SVG为例,viewBox尺寸为294×182:

1
2
3
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 294 182">
  <!-- ... -->
</svg>

我将SVG标签替换为<symbol>,并将其图形添加到我的SVG库中:

1
2
3
4
5
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="quick-draw-hat" viewBox="0 0 294 182">
    <g class="quick-draw-hat">[…]</g>
  </symbol>
</svg>

在多个SVG中使用symbol

我创建了两个SVG:

1
2
3
4
5
6
7
<svg class="svg-small" viewBox="0 0 1080 1440">
  <!-- ... -->
</svg>

<svg class="svg-large" viewBox="0 0 1920 1080">
  <!-- ... -->
</svg>

并在两者中插入对我的符号的引用:

1
2
3
4
5
6
7
<svg class="svg-small" viewBox="0 0 1080 1440">
  <use href="#quick-draw-hat" />
</svg>

<svg class="svg-large" viewBox="0 0 1920 1080">
  <use href="#quick-draw-hat" />
</svg>

定位符号

一旦使用<use>将符号放入布局中,下一步就是定位它们,这对于为不同屏幕尺寸创建替代布局尤其重要。符号的行为类似于<g>组,因此我可以使用width、height和transform等属性来缩放和移动它们:

1
2
3
4
5
6
7
<svg class="svg-small" viewBox="0 0 1080 1440">
  <use href="#quick-draw-hat" width="294" height="182" transform="translate(-30,610)"/>
</svg>

<svg class="svg-large" viewBox="0 0 1920 1080">
  <use href="#quick-draw-hat" width="294" height="182" transform="translate(350,270)"/>
</svg>

动画use元素

我想动画化角色的部分 - 比如Quick Draw的帽子倾斜和他的腿踢动。但是当我添加针对<symbol>内部元素的CSS动画时,什么也没有发生。

提示:您可以动画化<use>元素本身,但不能动画化<symbol>内部的元素。如果您希望单个部分移动,请将它们设为自己的符号并动画化每个<use>

事实证明,您不能样式化或动画化<symbol>,因为<use>创建了不容易定位的shadow DOM克隆。因此,我必须在每个符号内的库SVG中添加一个<g>元素:

1
2
3
4
5
<symbol id="quick-draw-hat" viewBox="0 0 294 182">
  <g class="quick-draw-hat">
    <!-- ... -->
  </g>
</symbol>

并使用属性子串选择器动画化它,针对use元素的href属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use[href="#quick-draw-hat"] {
  animation-delay: 0.5s;
  animation-direction: alternate;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-name: hat-rock;
  animation-timing-function: ease-in-out;
  transform-origin: center bottom;
}

@keyframes hat-rock {
from { transform: rotate(-2deg); }
to   { transform: rotate(2deg); } }

媒体查询显示控制

创建了两个可见的SVG - 一个用于小屏幕,一个用于大屏幕 - 最后一步是决定在哪个屏幕尺寸显示哪个版本。我使用CSS媒体查询来隐藏一个SVG并显示另一个。我默认显示小屏幕SVG:

1
2
.svg-small { display: block; }
.svg-large { display: none; }

然后我使用min-width媒体查询在64rem及以上切换到大型屏幕SVG:

1
2
3
4
@media (min-width: 64rem) {
  .svg-small { display: none; }
  .svg-large { display: block; }
}

总结

通过结合<symbol><use>、CSS媒体查询和特定的transform,我可以构建自适应SVG,在不重复内容、加载额外资源或依赖JavaScript的情况下重新定位其元素。我只需要在隐藏的符号库中定义每个图形一次。然后我可以在几个可见的SVG中根据需要重用这些图形。使用CSS进行布局切换,结果是快速且灵活的。

这提醒我们,网络上一些最强大的技术不需要大型框架或复杂的工具 - 只需要一些SVG知识和基础知识的巧妙使用。

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