精通-SVG-全-

精通 SVG(全)

原文:zh.annas-archive.org/md5/1F43360C7693B2744A58A3AE0CFC5935

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这本书适用于希望在项目中添加可伸缩、设备独立的动画、图像和可视化效果的 Web 开发人员和设计师。可伸缩矢量图形是一种由万维网联盟(W3C)于 1998 年推出的图像文件格式。多年来,它一直因浏览器兼容性差和不友好的 API 而不受重视。在过去几年里,它已成为现代 Web 开发工具包的重要组成部分。SVG 为现代 Web 提供了许多重要功能。例如,在多种设备分辨率的世界中,它提供了一条简单的路径,可以实现高质量的图像缩放,而无需为图像生成多个分辨率,也无需跳过复杂的标记模式。此外,作为基于 XML 的标记,它还允许轻松访问常见的 JavaScript 模式,用于创建高度交互式的界面。

本书将教会你如何使用 SVG 作为静态图像、在 CSS 中、作为 HTML 文档中的元素以及作为动画或可视化的一部分进行编写的基础知识。

这本书适合谁

这本书适用于对可伸缩矢量图形感兴趣的 Web 开发人员。它是从前端 Web 开发人员的角度编写的,但任何有 JavaScript、CSS 和基于 XML 的语法经验的人都应该能够理解本书。

不需要有 SVG 的先验经验。

本书涵盖的内容

第一章《介绍可伸缩矢量图形》介绍了 SVG 的基础知识,并将向你展示一些使用该格式的基本示例。

第二章《开始使用 SVG 创作》详细介绍了创作 SVG 的基本概念。

第三章《深入挖掘 SVG 创作》介绍了更高级的 SVG 创作概念,包括变换、裁剪和遮罩,以及将 SVG 元素导入文档。

第四章《在 HTML 中使用 SVG》进一步介绍了在 HTML 文档中使用 SVG 元素和 SVG 图像的细节。

第五章《使用 SVG 和 CSS》介绍了在 CSS 中使用 SVG 图像,取代 PNG 和 Gif 在现代 Web 开发工具包中的使用。本章还介绍了使用 CSS 修改 SVG 元素的多种方法。

第六章《JavaScript 和 SVG》通过介绍常见的文档对象模型方法,教会读者基本的 JavaScript SVG 应用程序接口,这些方法允许开发人员访问和操作 SVG 属性。

第七章《常见的 JavaScript 库和 SVG》教授了如何从常见的库和框架(包括 jQuery、AngularJS、Angular 和 ReactJS)中与 SVG 进行交互的基础知识。

第八章《SVG 动画和可视化》介绍了使用 SVG 进行可视化和动画的示例。

第九章《辅助库 Snap.svg 和 SVG.js》介绍了两个当前帮助处理常见 SVG 任务的库:Snap.svg 和 SVG.js。

第十章《使用 D3.js 工作》介绍了 D3 的基本用法,并通过一些简单的示例来激发你对这个强大库的兴趣。

第十一章《优化 SVG 的工具》专注于优化 SVG 的不同工具。

为了充分利用本书。

本书假设你具有 HTML、XML、CSS 和 JavaScript 的知识。了解 Node.js 和基于 npm 的开发也会有所帮助。

在开始之前,确保您已安装了 Node.js 会很有帮助。您还需要一个文本编辑器。本书中的示例是使用 Visual Studio Code 编写的,但任何文本编辑器都可以。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择 SUPPORT 选项卡。

  3. 点击代码下载和勘误。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩软件解压缩文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-SVG。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788626743_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"

代码块设置如下:

<svg  width="350" height="150" viewBox="0 0 350 150" version="1.1">
    <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,.5)"/>
    <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,.5)" 
     transform="translate(10)" />
    <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,.5)" 
     transform="translate(75,0)" />
</svg>

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

types {
    image/svg+xml svg svgz;
}

任何命令行输入或输出都以以下方式编写:

 $ npx create-react-app react-svg

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中显示为这样。例如:"在本文档中,我们创建了一个风格化的字母 R。"

警告或重要说明会显示为这样。

提示和技巧会显示为这样。

第一章:介绍可缩放矢量图形

可缩放矢量图形SVG)是现代 Web 开发中最强大的组件之一。如果使用得当,它可以解决与图像和用户界面的设计、开发和交付相关的常见问题。

SVG 是一种基于 XML 的标记语言,用于定义图像。就像 HTML 是用于文本一样,SVG 是用于图像的。

SVG 非常灵活。它可以作为独立图像实现,并用作图像的src,也可以作为 CSS 中的背景图像,如 PNG、GIF 或 JPG。它也可以直接嵌入到 HTML 页面中,并通过 CSS 或 JavaScript 进行操作,以创建动画、可视化和交互式图表。

因此,如果 SVG 如此重要并且可以做这么多事情,为什么它还没有被更广泛地使用呢?为什么感觉我们只是挖掘了它的表面?为什么它仍然感觉像是一件的东西?

问题是,并不是每个人都知道 SVG 的所有功能,也不是每个了解其功能的人都能以最佳方式实现 SVG 解决方案。本书旨在帮助所有对使用 SVG 感兴趣的人克服这些障碍,掌握这项重要的技术。

SVG 在现代 Web 开发技术中的地位经历了曲折的道路。SVG 于 1999 年发布(比 XHTML 还要早),由于当时主导的 Internet Explorer 浏览器缺乏支持,SVG 在接下来的十年中一直处于低迷状态。几年前,随着 JavaScript 库(如 Raphaël)的出现,为旧版本的 IE 添加了编程回退支持,这项技术开始受到青睐,而这种趋势自那时以来一直在增强。幸运的是,潮流已经完全扭转。所有现代版本的 Internet Explorer 和 Edge 都支持 SVG,所有浏览器制造商都对这项技术给予了强大的支持,包括 Chrome 和 Firefox。

通过本章结束时,您将了解 SVG 在各种形式中的基础知识。您将能够在网页和 CSS 中自信地使用现有的 SVG 图像,并且您将在掌握 SVG 的过程中迈出良好的一步。

本章将涵盖以下主题:

  • SVG 的基本语法和矢量图形介绍

  • 将 SVG 用作图像的src文件的原因和方法

  • SVG 作为 CSS 背景图像的基本用法

  • 直接在文档中嵌入 SVG 的好处和区别

  • Modernizr 和特性检测简介

创建一个简单的 SVG 图像

如果您对 HTML 有所了解,那么 SVG 文档的基础对您来说将是熟悉的。所以让我们早点揭开神秘面纱,看一看一个简单的 SVG 文档。

以下代码示例显示了 SVG 的基本结构。第一个元素是标准的xml声明,表示接下来的内容应该被解析为 XML 文档。第二个元素是乐趣的开始。它定义了根 SVG 元素(就像 HTML 文档中有一个根 HTML 元素一样)。heightwidth定义了文档的固有尺寸。XML NameSpace (xmlns)是对定义当前 XML 元素的模式的引用。您将在下一章中更详细地了解viewBox。SVG 元素上还有许多其他可能的属性。您将在本书中更多地了解它们。

在这个第一个例子中,在 SVG 元素之后,有一个单独的 SVG text元素。text元素,就像 SVG 元素一样,有许多可能的属性,你将在阅读本书的过程中了解到。在这种情况下,有四个与元素显示相关的属性。xy属性表示文本元素左上角的位置,作为坐标平面上的点。font-family映射到同名的常见 CSS 属性,定义应该用于显示文本的特定字体。font-size也映射到同名的常见 CSS 属性。

接受长度值的属性(在这个例子中是widthheightfont-size)是不带单位的(例如pxem%)。当这些值作为属性呈现时,单位是可选的。如果没有提供单位,这些值被指定为用户空间中的用户单位。你将在本书中了解更多关于 SVG 中值的计算方式。现在,只需记住,在实践中,用户单位将等同于像素。

最后,是text元素的内容,简单的消息 Hello SVG:

<?xml version="1.0" encoding="UTF-8"?>
<svg width="250" height="100" viewBox="0 0 250 100" version="1.1" xmlns=”http://www.w3.org/2000/svg”>
<text x="0" y="50" font-family="Verdana" font-size="50">
    Hello SVG
  </text>
</svg>

保存为1-1-hello-world.svg并在浏览器中打开,前面的标记呈现如下屏幕截图:

现在你已经看到了 SVG 文档的最基本示例,让我们以各种方式来看一下 SVG 图像和元素的基本用法。

使用 SVG 作为内容图像

在这一部分,你将学习 SVG 图像的最基本用法,就像你使用 JPG、PNG 或 GIF 一样,作为img元素的src。如果你已经做过任何 HTML 工作,那么你会知道如何做到这一点,因为它只是一个图像元素,但你应该开始考虑所有你可以使用 SVG 的不同方式,这是一个重要的方式。

看下面的代码示例,img元素并没有什么特别之处。有一个指向 SVG 图像的srcheightwidth定义图像的尺寸,还有一个alt属性,为屏幕阅读器和其他图像无法显示的情况提供图像的文本表示:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG - Inserting an SVG Image into an HTML
         Document</title>
    </head>
    <body>
      <img src="img/1-2-circles.svg" width="250" height="250" alt="an image
        showing four circles lined up diagonally across the screen">
    </body>
</html>

在浏览器中运行上述代码将呈现如下内容:

可能会有一个小问题,不是所有的 web 服务器默认设置了正确的 SVG MIME 类型。如果 MIME 类型设置不正确,一些浏览器将无法正确显示 SVG 图像。一个常见的例子是,微软的 IIS 可能需要更改特定的配置设置(docs.microsoft.com/en-us/iis/manage/managing-your-configuration-settings/adding-ie-9-mime-types-to-iis)才能正确地提供 SVG 图像。正确的 MIME 类型是image/svg+xml

用代码绘图

在学习其他基本实现之前,值得更深入地看一下前面的屏幕截图。它不仅仅是像第一个例子那样的文本(毕竟,你可以在 HTML 中完成),它显示了四个圆对角排列在画布上。让我们来看看该图像的源代码,并学习 SVG 中的第一个视觉元素,circle元素。

以下代码示例显示了circle的操作。它还显示了标记属性值的简单更改如何创建视觉上有趣的图案。其中有五个circle元素。所有这些都利用了四个新属性。cxcy表示元素在坐标平面上的中心x和中心y坐标。r表示圆的半径。fill定义了填充circle的颜色。fill接受任何有效的 CSS 颜色值(developer.mozilla.org/en-US/docs/Web/CSS/color_value)。在这种情况下,我们使用了一个红色绿色蓝色alphaRGBA)值来填充这个纯红色的变化。前几个值保持不变,而第四个值,alpha,每次从.125加倍到1(完全不透明)。同样,cxcyr每次加倍。这产生了您之前看到的图案。这不是最复杂的 SVG 图像,但它确实向您展示了基本 SVG 元素的使用和理解有多容易:

<?xml version="1.0" encoding="UTF-8"?>
<svg width="250" height="250" viewBox="0 0 250 250" version="1.1" >
       <circle cx="12.5" cy="12.5" r="6.25" fill="rgba(255,0,0,.125)">
       </circle>
       <circle cx="25" cy="25" r="12.5" fill="rgba(255,0,0,.25)">
       </circle>
       <circle cx="50" cy="50" r="25" fill="rgba(255,0,0,.5)"></circle>
       <circle cx="100" cy="100" r="50" fill="rgba(255,0,0,.75)">
       </circle>
       <circle cx="200" cy="200" r="100" fill="rgba(255,0,0,1)">
       </circle>
</svg>

可伸缩的矢量图形

现在您已经看到了使用 SVG 创建的绘图示例,可能有必要花一点时间解释 SVG 中的VG以及为什么这使文件格式可伸缩

对于光栅(位图)文件格式,您可能熟悉的格式有 JPG、PNG 或 GIF。您可以将图像数据视为逐像素存储,因此图像中的每个点都存储在文件中,并由浏览器或图形程序逐像素和逐行读取。图像的大小和质量受到创建时的大小和质量的限制。

所有位图文件格式都有优化,限制了实际存储的数据量。例如,GIF 使用 LZ77 算法将冗余像素折叠到一个回指器和参考像素中。想象一下,如果您的图像有100个纯黑像素排成一行。该算法将搜索图像以找到相同字节的序列,当遇到序列时,算法将向后搜索文档,以找到该模式的第一个实例。然后,它将用指令(回指器)替换所有这些像素,指示向后搜索多少个字符以及复制多少像素以填充相同字节的数量。在这种情况下,它将是100(要搜索的像素)和1(要复制的像素)。

矢量图形,另一方面,是由矢量和控制点定义的。为了显著简化,您可以将矢量图形视为描述线条形状的一组数字。它们可能是一组特定的点,也可能是,就像之前的圆的情况一样,一组关于如何创建特定类型对象的指令。circle元素并不存储组成圆的每个像素。它存储用于创建圆的参数

为什么这很酷?一个原因是因为它只是一组定义形状的指令,您可以放大或缩小,渲染引擎将根据需要计算新值。因此,矢量图形可以无限缩放而不会失去保真度。

如果这一切对您来说很困惑,不要担心。您与它们一起工作得越多,您就会越熟悉矢量图形的工作方式。与此同时,以下一组示例和图表将有助于说明差异。首先,看看以下标记。它表示四个图像,使用完全相同的 SVG 图像作为源。该图像代表 SVG 标志。尺寸设置为图像的自然大小,然后是2x4x8x,图像的自然大小:

      <img src="img/svg-logo-h.svg" width="195" height="82" alt="The SVG 
       logo at natural dimensions">
      <img src="img/svg-logo-h.svg" width="390" height="164" alt="The SVG 
       logo 2x">
      <img src="img/svg-logo-h.svg" width="780" height="328" alt="The SVG
       logo 4x">
      <img src="img/svg-logo-h.svg" width="1560" height="656" alt="The SVG
       logo 8x">

在浏览器中呈现,该标记产生以下结果。请注意,它一直清晰到8x,即原始大小:

现在,再看看相同的标记,这次是 PNG 格式。它遵循相同的模式:

      <img src="img/svg-logo-h.png" width="195" height="82" alt="The SVG
       logo at 'natural' dimensions">
      <img src="img/svg-logo-h.png" width="390" height="164" alt="The SVG 
       logo 2x">
      <img src="img/svg-logo-h.png" width="780" height="328" alt="The SVG
       logo 4x">
      <img src="img/svg-logo-h.png" width="1560" height="656" alt="The SVG
       logo 8x">

但现在,看结果。注意,在自然级别上,SVG 和 PNG 之间没有区别。PNG 中的像素足以匹配 SVG 版本中定义的矢量线。此外,注意随着图像变大,图像变得越来越糟。浏览器无法从位图格式中获取更多信息(更多像素)来填补较大尺寸的细节。它只是放大它拥有的像素,结果非常糟糕(特别是在8x级别):

在 CSS 中使用 SVG

SVG 的常见用法是作为 CSS 中的背景图像。在响应式网页设计RWD)方面,这种方法在文件大小和可伸缩性方面都有好处。在今天的多设备、多形态因素的世界中,能够以一系列设备尺寸和分辨率(包括高像素密度设备)提供高质量图像的能力是非常重要的。虽然对于光栅显示图像有优化的解决方案(以picture元素和srcsetsizes属性的形式)并且你可以使用媒体查询在 CSS 中呈现不同的图像或图像尺寸,但是能够为所有设备做一张图像是非常重要的。CSS 中的 SVG 使我们能够轻松实现这一点

虽然你将在第五章中学习 SVG 和 CSS 的交集,使用 SVG 和 CSS,现在让我们看一个基本的例子来激发你的兴趣。

以下页面有一个类为 header 的div标签。这里唯一需要注意的是background属性的url值中引用了一个 SVG 文件:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- Using SVG images in CSS</title>
        <style type="text/css">
            .header {
                color: #ffffff;
                background: url(1-3-gradient.svg) repeat-x;
                width: 500px;
                height: 40px;
                text-align: center;
            }
        </style>
    </head>
    <body>
      <div class="header"><h1>CSS!</h1></div>
    </body>
</html>

这段代码在浏览器中运行时会产生以下效果。这个简单的例子与任何其他 CSS 实现没有区别,它将在不损失渐变平滑度的情况下适应最高像素每英寸的显示。这是通过简单地使用 SVG 实现的:

SVG 中的渐变

当你继续学习基本的 SVG 用法时,我将继续引入 SVG 本身创作的新概念。我将向你介绍的下一个功能是defs部分、gradient元素和rect元素。

以下示例显示了前一个示例中 SVG 元素的源。除了根svg元素本身之外,其他所有内容都与前一个示例不同。

首先是defs元素。defs是一个组织元素,旨在保存以后在文档中使用的图形对象的定义。我们立即遇到了linearGradient元素,它定义了(你猜对了!)线性渐变。x1x2y1y2定义了渐变的渐变向量。你将在第二章中了解更多,使用 SVG 和 CSS,但现在只需知道它定义了渐变的方向。默认值是0在左边,1在右边。将x2设置为0y2设置为1会将角度从水平左到右的渐变改变为垂直上到下的渐变。

渐变的外观实际上是由子stop元素定义的。每个都有两个属性,offsetstop-color。偏移接受百分比或01之间的数字,表示渐变停止在渐变向量的整体上的位置。这个例子是最简单的:0%处有一种颜色,100%处有另一种颜色。stop-color接受任何有效的颜色值:

<svg width="10" height="40" viewBox="0 0 10 40" version="1.1" >
 <defs>
 <linearGradient id="gradient" x1="0" x2="0" y1="0" y2="1">
 <stop offset="0%" stop-color="#999999"/>
 <stop offset="100%" stop-color="#000000"/>
 </linearGradient>
 </defs>
 <rect x="0" y="0" width="10" height="40" fill="url(#gradient)"/>
</svg>

由于这些只是关于如何渲染渐变的说明,在这种情况下可以拉伸和移动背景图像而不会损失保真度。浏览器将计算新值并渲染新的完美渐变。

以下示例显示了对 CSS 的调整,将标题拉伸到浏览器高度的一半(使用vh单位),并强制标题背景图像填充可用空间(background: size: contain):

<!doctype html>
<html lang="en">
 <head>
   <meta charset="utf-8">
   <title>Mastering SVG- Using SVG images in CSS</title>
   <style type="text/css">
  .header {
   color: #ffffff;
   background: url(1-3-gradient.svg) repeat-x;
   width: 500px;
   height: 50vh;
   text-align: center;
   background-size: contain;
  }
  </style>
 </head>
 <body>
   <div class="header"><h1>CSS!</h1></div>
 </body>
</html>

如您在以下截图中所见,相同的背景图像可以轻松调整大小。正如您将学到的那样,对 SVG 可以做的任何其他事情也是如此。

直接在 HTML 文档中嵌入 SVG

在我看来,SVG 最令人兴奋的用途是作为 HTML 文档中的内联元素。虽然您将了解 SVG 图像作为单独的文件格式以及 SVG 图像可以用于开发现代 Web 应用程序的所有方式,但本书的大部分内容将向您展示如何与直接嵌入文档的 SVG 元素进行交互。这很重要,因为无法对外部引用的 SVG 文件的各个元素进行动画或以其他方式进行操作;只有在页面上直接(通过文档对象模型DOM))可用 SVG 元素时才可能。

以下示例显示了一个简单的内联 SVG 图像,其中包含三个圆圈,并展示了在使用内联 SVG 时您拥有的最强大的工具之一:CSS!CSS 可以用来以与样式常规 HTML 元素相同的方式来样式化 SVG 元素。这打开了一系列可能性。这里使用的属性可能对您来说是新的,因为它们是特定于 SVG 的,但就像您习惯的background-colorborder属性一样,您可以使用 CSS 调整 SVG 元素的基本外观和感觉。在下一个示例中,CSS 为所有圆圈定义了默认的fill颜色,为第二个圆圈添加了border,然后更改了第三个圆圈的fill颜色。如果您还没有计划如何使用 CSS 来操作 SVG 元素,那么请放心,阅读完第五章之后,您将有很多想法:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG - Using SVG images in CSS</title>
        <style type="text/css">
            circle {
              fill: rgba(255,0,0,1);
            }
            .first {
              opacity: .5;
            }
            .second {
              stroke-width: 3px;
              stroke: #000000;
            }
            .third {
              fill: rgba(0,255,0,.75);
            }
        </style>
    </head>
    <body>
      <svg width="400" height="250" viewBox="0 0 400 250" version="1.1"
       >
        <circle cx="100" cy="100" r="25" class="first"></circle>
        <circle cx="200" cy="100" r="25" class="second"></circle>
        <circle cx="300" cy="100" r="25" class="third"></circle>
        </svg>
    </body>
</html>

打开浏览器将显示所有 CSS 的结果:

特性检测和 Modernizr

尽管全球网络对 SVG 的整体支持(caniuse.com/#search=svg)现在非常高,但并不一致,仍然存在不支持 SVG 的浏览器。这就是特性检测库 Modernizr 可以派上用场的地方。如果您的用户群广泛,或者您正在使用更新的(甚至是实验性的)功能,您可以使用 Modernizr 来检测浏览器对重要功能的兼容性,并相应地调整您的代码。

这种工作有两种方式。一种是 Modernizr 可以放置在 HTML 元素上的类。另一种是全局 Modernizr 对象,其中包含所有测试结果作为布尔值。在我们继续之前,我将向您展示这两种工具的示例。

Modernizr 项目提供了数百个测试。由于某些测试相当昂贵(在计算所需资源方面),因此在使用 Modernizr 时,您希望仅使用您的应用程序所需的测试。在这种情况下,我创建了一个特定的 Modernizr 构建,用于测试多个 SVG 功能,而不测试其他内容。将此文件添加到 HTML 页面后,将向 HTML 元素添加类,指示对各种 SVG 功能的支持

以下是 Microsoft Edge 中 HTML 元素的输出。 no-smil类表示 Edge 不支持同步多媒体集成语言SMIL),但支持我们正在测试的其他所有内容:

<html class=" svg svgclippaths svgforeignobject svgfilters
 no-smil inlinesvg svgasimg" lang="en">

最新 Chrome 版本的输出显示支持所有测试功能:

<htmlclass=" svg svgclippaths svgforeignobject svgfilters smil 
 inlinesvg svgasimg" lang="en" >

最后,Internet Explorer 8(IE8)根本不支持 SVG:

<HTML class=" no-svg no-svgclippaths no-svgforeignobject no-svgfilters 
 no-smil no-inlinesvg no-svgasimg" lang="en">

使用这些类可以让您为 IE8 提供 PNGfallback功能,例如为 CSS 背景图像提供支持:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- Modernizr</title>
        <style type="text/css">
            .header {
                color: #ffffff;
                background: url(1-3-gradient.svg) repeat-x;
                width: 500px;
                height: 40px;
                text-align: center;
            }
            .no-svg .header {
                background: url(1-3-gradient.png) repeat-x;
              }
        </style>
    </head>
    <body>
      <div class="header"><h1>CSS!</h1></div>
    </body>
</html>

正如前面提到的,Modernizr 还公开了一个全局 Modernizr JavaScript 对象,其中包含每个可用测试的布尔值。以下示例显示了如何访问该布尔值,并使用if语句对代码进行近似处理,具体取决于 SVG 是否受支持:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- Monderizr JavaScript Object</title>
        <script src="img/modernizr-custom.js"></script>
      </head>
    <body>
      <script>
        if (Modernizr.svg){
          // do things with SVG
        } else {
          //create a non-SVG fallback
        }
      </script>
    </body>
</html>

一般来说,本书的其余部分不会专注于老版本浏览器的回退,但如果你在需要支持广泛的浏览器和设备的环境中工作,了解它们的存在是很有用的。

总结

在本章中,我们学习了关于 SVG 的基础知识,包括几个 SVG 特定的元素,如circletext,以及用于创建 SVG 渐变的元素。我们还学习了在 HTML 文档中以及在 CSS 中将 SVG 用作背景图像的几种方法。

我们还学习了关于 Modernizr 特性检测库以及如何使用它为不支持 SVG 或特定 SVG 功能的浏览器创建回退

在第二章中,开始使用 SVG 进行创作,你将学习更多关于 SVG 功能的知识,扩展你对创作 SVG 文档的了解。

第二章:使用 SVG 进行创作的入门

现在您已经初步了解了 SVG,是时候更深入地了解常见的 SVG 元素及其用法了。本章将重点介绍最常见的 SVG 元素及其用法,深入介绍您已经学习过的一些元素,并介绍您在创建 SVG 图像时将使用的许多其他元素。

本章将涵盖以下主题:

  • 基本 SVG 形状

  • SVG 定位系统

  • 渐变和图案

  • 使用软件程序生成的 SVG 图像,例如 Adobe Illustrator、Inkscape 和 Sketch

SVG 中的定位

如您在第一章中所见,介绍可伸缩矢量图形,SVG 元素使用坐标平面定位系统。SVG 文档中的元素使用xy坐标来定位。这对您来说应该很熟悉,因为您在几何课程中或者更具体地说,在网页上使用 CSS 时,您已经习惯了绝对定位的元素。以下代码展示了您已经在圆元素和矩形元素上看到的定位方案的两种变化,圆元素使用(cxcenter x)和(cycenter y)属性来基于圆的中心放置circle元素,而rect元素将使用xy属性来在坐标平面上放置正方形的左上角:

     <svg  width="350" height="150"
       viewBox="0 0 350 150" version="1.1"> 
        <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,1)"/> 
        <rect x="200" y="25" width="100" height="100" 
         fill="rga(0,0,255,1)"/> 
      </svg> 

在浏览器中呈现的效果如下:

除了使用两个中心属性来基于其中心放置元素,xy,这应该看起来就像在 CSS 中定位元素一样。有趣的地方在于heightwidth的交集以及 SVG 元素本身的viewBox属性的值。

SVG 中的 viewBox 和视口

heightwidth属性定义了 SVG 元素的视口。视口可以被视为与浏览器中的视口相同。它定义了 SVG 文档的可见尺寸。底层 SVG 文档的尺寸可以大于视口,并且与 HTML 一样,元素可以完全不在屏幕上。所有可见的内容都在视口的尺寸内。

如果您只设置 SVG 元素的heightwidth属性,并且不使用viewBox属性,它将以与您在 CSS 中使用的方式相同的方式运行。在前面的例子中,视口坐标系统将以坐标(0,0)开始,并以(350, 150)结束。

在本书中,坐标将呈现为(x值,y值)。

在这种情况下,每个用户单位默认为屏幕上的一个像素。

viewBox属性允许您更改初始视口坐标系统。通过重新定义该坐标系统,您可以以有趣的方式移动和缩放底层 SVG 文档。让我们看一些例子,而不是试图描述可能发生的事情。

到目前为止,我们展示的每个例子都使用了viewBox属性,并且它被设置为与视口的heightwidth属性的尺寸相匹配。如果我们改变 SVG 元素的heightwidth属性,并且不改变viewBox以匹配,会发生什么?添加一个新的 SVG 元素,其height属性和width属性等于原始值的两倍,会创建一个两倍大小的图像的第二个版本:

     <svg  width="700" height="300" 
       viewBox="0 0 350 150" version="1.1"> 
        <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,1)"/> 
        <rect x="200" y="25" width="100" height="100" 
         fill="rga(0,0,255,1)"/> 
      </svg> 

在浏览器中的效果如下。如您所见,视口已经加倍,但由于viewBox具有相同的尺寸,circlerect元素上的确切坐标仍会创建图像的放大版本。在这种情况下,用户单位不再等同于一个像素,但 SVG 元素内部的计算仍然保持不变:

您可以将其放大到任意大小,它都会完美呈现。

如果我们调整viewBox属性本身会发生什么?viewBox属性的值代表什么?

viewBox属性有四个参数:min-xmin-ywidthheightmin-xmin-y定义了viewBox的左上角。现在,widthheight确定了viewBox的宽度和高度。调整这些值可以显示它们如何与视口的高度和宽度交互。前两个示例改变了视口坐标系的xy位置。第一个示例将其正向偏移了 20%(70 和 30 分别是 SVG 宽度和高度的 20%)。第二个示例将其负向偏移了 20%。第三个示例改变了viewBox属性的宽度和高度,将其缩小了一半:

<svg  width="350" height="150" viewBox="70 30 350 150" version="1.1"> <circle cx="100" cy="75" r="50"
  fill="rgba(255,0,0,1)"/> <rect x="200" y="25" width="100" 
  height="100" fill="rga(0,0,255,1)"/> </svg> 
<svg  width="350" height="150" 
 viewBox="-70 -30 350 150" version="1.1"> <circle cx="100" cy="75" 
 r="50" fill="rgba(255,0,0,1)"/> <rect x="200" y="25" width="100" height="100" fill="rga(0,0,255,1)"/> </svg> 
<svg  width="350" height="150" 
 viewBox="0 0 175 75" version="1.1"> <circle cx="100" cy="75" r="50"
 fill="rgba(255,0,0,1)"/> <rect x="200" y="25" width="100" height="100" 
 fill="rga(0,0,255,1)"/> </svg> 

在浏览器中呈现,你可以看到viewBox属性的这些变化效果。偏移移动了圆和正方形,使其相对于视口的左上角更接近。将viewBox属性的大小缩小一半,并保持rectcircle的大小不变,实际上使渲染元素的大小加倍。视口保持相同大小,因此viewBox属性和相关的用户单位按比例放大了两倍。其中的所有元素都按需放大:

以下图表显示了更深入的工作原理(黑色轮廓覆盖层代表viewBox视口):

在 SVG 中仍有更多关于定位的知识需要学习,但我们将在本书的其余部分逐步解决这些问题。

现在让我们更深入地了解一些构成 SVG 体验的其他元素。

引入路径

在 SVG 规范中,最重要的元素是path元素。path允许您使用作为d属性值传递的一系列命令来绘制线条和形状。还记得我提到过 SVG 采用的最大障碍之一是缺乏友好的 API 吗?这个path元素很可能是整个规范中最大的痛点。您可能在d属性中看到的值可能非常密集且难以阅读。有多难以阅读?看看 SVG 标志中的S元素:

<path id="S" d="M 5.482,31.319 C2.163,28.001 0.109,23.419 0.109,18.358 C0.109,8.232 8.322,0.024 18.443,0.024 C28.569,0.024 36.782,8.232 36.782,18.358 L26.042,18.358 C26.042,14.164 22.638,10.765 18.443,10.765 C14.249,10.765 10.850,14.164 10.850,18.358 C10.850,20.453 11.701,22.351 13.070,23.721 L13.075,23.721 C14.450,25.101 15.595,25.500 18.443,25.952 L18.443,25.952 C23.509,26.479 28.091,28.006 31.409,31.324 L31.409,31.324 C34.728,34.643 36.782,39.225 36.782,44.286 C36.782,54.412 28.569,62.625 18.443,62.625 C8.322,62.625 0.109,54.412 0.109,44.286 L10.850,44.286 C10.850,48.480 14.249,51.884 18.443,51.884 C22.638,51.884 26.042,48.480 26.042,44.286 C26.042,42.191 25.191,40.298 23.821,38.923 L23.816,38.923 C22.441,37.548 20.468,37.074 18.443,36.697 L18.443,36.692 C13.533,35.939 8.800,34.638 5.482,31.319 L5.482,31.319 L5.482,31.319 Z"/> 

不知道发生了什么,是不可能解析的,即使知道d属性的规则,也很难跟踪。

让我们看一个更简单的例子,以便你能理解语法。在这个文档中,我们创建了一个风格化的字母 R。以下是如何阅读d属性的指令:

  1. (M)ove to point (100,100).

  2. 画一条(L)线到(100,300)

  3. 画一条(L)线到(150,300)

  4. 画一条(L)线到(150,150)

  5. 从当前点绘制(S)平滑立方贝塞尔曲线到点(150,175),第二个控制点为(250,150)。控制点提供用于绘制曲线的方向信息。这个版本的立方贝塞尔curveto指令实际上是控制点被反射的简写。在其他格式中,可以定义指向不同方向的多个控制点。这将创建一个更复杂的曲线。

  6. 画一条(L)线到(200,300)

  7. 画一条(L)线到(250,300)

  8. 画一条(L)线到(225,225)

  9. 从当前起始点绘制(S)平滑立方贝塞尔曲线到点(100,100),第二个控制点为(350,100)

<svg  width="500" height="500" viewBox="0 0 500 500" version="1.1"> 
        <path d="M100,100 L100,300 L150,300 L150,150 S250,150,175,200 L200,300 L250,300 L225,225 S350,100,100,100" stroke-width="1" stroke="#003366" fill="#cccccc"></path> 
</svg> 

在浏览器中呈现,这些命令产生以下结果:

这一系列的说明仍然很复杂,甚至没有涉及path元素的所有可能选项。好消息是,当您使用 SVG 时,大多数时候这些复杂的path将被生成 - 要么由您自己(使用图形 SVG 编辑器),要么通过 JavaScript。因此,实际上,您只需要能够理解说明和它们的用法。您不需要坐在那里逐条解析这些数据指令。

更多关于基本形状

现在您已经了解了path,让我们来看看 SVG 宇宙中更直接的部分,并且让我们检查一些更基本的形状。您已经了解了circlerect。让我们看看一些更基本的形状。

线元素

path元素允许您使用一长串的说明来绘制您能想象到的任何东西。值得庆幸的是,有许多方便的元素定义了比path元素更容易处理的常见形状。其中您将学习的第一个是line元素。

以下示例在一个500500的正方形上绘制了一个网格。这里使用的line元素需要五个参数:x1y1x2y2strokexy坐标表示线的起点(x1y1)和终点(x2y2)。这个 SVG 文档在一个500像素的正方形中绘制了每边100`像素的网格:

    <svg version="1.1"  
        width="500" height="500" viewBox="500 500 0 0"> 
        <line stroke="#000000" x1="0" y1="0" x2="0" y2="500" /> 
        <line stroke="#000000" x1="100" y1="0" x2="100" y2="500" /> 
        <line stroke="#000000" x1="200" y1="0" x2="200" y2="500" /> 
        <line stroke="#000000" x1="300" y1="0" x2="300" y2="500" /> 
        <line stroke="#000000" x1="400" y1="0" x2="400" y2="500" /> 
        <line stroke="#000000" x1="500" y1="0" x2="500" y2="500" /> 
        <line stroke="#000000" x1="0" y1="0" x2="500" y2="0" /> 
        <line stroke="#000000" x1="0" y1="100" x2="500" y2="100" /> 
        <line stroke="#000000" x1="0" y1="200" x2="500" y2="200" /> 
        <line stroke="#000000" x1="0" y1="300" x2="500" y2="300" /> 
        <line stroke="#000000" x1="0" y1="400" x2="500" y2="400" /> 
        <line stroke="#000000" x1="0" y1="500" x2="500" y2="500" /> 
      </svg> 

在浏览器中呈现,前面的标记产生了以下网格:

顺便说一句,生成这样的网格对生成和调试 SVG 文档很有帮助。在网格上有更细粒度的网格时,您可以更容易地确定屏幕上计算或手动生成的位置。

椭圆元素

ellipse就像circle一样,只是它需要两个半径参数,rxry分别代表xy的半径。由于需要额外的半径参数,否则我们只会画一个标准的圆:

      <svg width="250" height="100" viewBox="0 0 250 100" 
         > 
        <ellipse cx="125" cy="50" rx="75" ry="25" 
         fill="rgba(255,127,0,1)"/> 
      </svg> 

以下是直接标记的输出:

多边形元素

polygon元素创建由多条直线组成的封闭形状,从初始x,y坐标开始,并以坐标平面上的最终点结束。points属性接受坐标平面上的点列表来定义polygon元素。polygon元素的最终点会自动连接到第一个点。以下代码示例绘制了一个星星:

<svg width="240" height="240" viewBox="0 0 240 240" 
  > 
        <polygon points="95,95 120,5 150,95 235,95 165,150 195,235
         120,180 50,235 75,150 5,95" fill="rgba(0,0,255,1)"></polygon> 
</svg> 

以下显示了前面 SVG 元素的输出:

对于polygonpolyline,这只是一个建议,而不是将x,y对用逗号分隔的要求。

以下代码在程序上等同于前面的例子(尽管更难阅读)。它呈现了完全相同的形状:

<svg width="240" height="240" viewBox="0 0 240 240" 
  > 
   <polygon points="95 95 120 5 150 95 235 95 165 150 195 235 120 
     180 50 235 75 150 5 95" fill="rgba(0,0,255,1)"></polygon> 
 </svg> 

折线元素

polyline元素创建由多条直线组成的开放形状。points属性接受坐标平面上的x,y点列表来定义polyline。以下代码示例跟踪了天空中龙座的图案:

<svg width="800" height="600" viewBox="0 0 400 300" 
   > 
   <polyline points="360,60 330,90 295,160 230,220 190,217
    175,180 155,130 155,60 135,30 100,25 90,55 65,170 80,195 
    65,220 35,210 65,170" fill="none" stroke="white" stroke-width="3"> 
    </polyline> 
</svg> 

在浏览器中运行,前面的例子看起来是这样的:

更多关于填充和描边

您已经在大多数示例中看到了它们的使用,现在让我们更全面地了解一下填充和描边。这些表示属性对 SVG 很重要,特别是在动态工作时,因为与编写动态 CSS 相比,直接操作元素要容易得多。

fillstroke被统称为paint属性。fill设置对象的内部颜色,stroke设置对象周围绘制的线的颜色。正如您已经看到的,它们可以接受任何有效的 CSS 颜色值。它们还可以接受对绘画服务器元素的引用(这些是hatchlinearGradientmeshgradientpatternradialGradientsolidcolor),这些元素定义了元素的绘画样式。您已经看到了其中一个(linearGradient),很快将了解更常见的支持。然而,在您这样做之前,现在是时候看一看一些控制线条外观和拟合的特定于描边的属性了。

stroke-dasharray

stroke-dasharray属性定义了一个逗号和/或空格分隔的长度或百分比列表,指定了用于描边线的虚线和间隙的交替模式。以下示例显示了几个不同的示例。第一个是一系列 10 像素的开和 5 像素的关。第二个示例根据斐波那契数列打开和关闭像素。第三个系列根据质数系列打开和关闭像素:

<svg width="400" height="300" viewBox="0 0 400 300" 
  > 
  <rect x="50" y="20" width="300" height="50" fill="none" 
    stroke="#000000" stroke-width="4"  stroke-dasharray="10 5"></rect> 
  <rect x="50" y="80" width="300" height="50" fill="none" 
   stroke="#000000" stroke-width="4"  stroke-dasharray="1, 2, 3, 5, 8, 
    13"></rect> 
  <rect x="50" y="140" width="300" height="50" fill="none" 
    stroke="#000000" stroke-width="4"  stroke-dasharray="2, 3, 5, 7, 
     11, 13, 17, 19"></rect> 
 </svg> 

在浏览器中呈现,上述代码产生以下示例:

如果提供的值作为属性的值的数量是奇数,则列表将重复以产生偶数个值。这可能不会产生您期望的模式,因为值可能会从虚线转移到空格,并产生意想不到的结果。在以下示例中,单个值10产生10开和10关,这可能是您预期的结果。另一方面,"15,10,5"模式产生15开,10关,5开,15关,10开和5关。如果您期望模式始终将15作为“开”,那么这可能会让您感到惊讶。

 <svg width="400" height="300" viewBox="0 0 400 300"
    > 
    <rect x="50" y="20" width="300" height="50" fill="none" 
     stroke="#000000" stroke-width="4"  stroke-dasharray="10"> 
    </rect> 
    <rect x="50" y="80" width="300" height="50" fill="none" 
      stroke="#000000" stroke-width="4"  stroke-dasharray="15,10,5">
    </rect> 
  </svg> 

您可以在浏览器中看到这一点。这可能是您想要的外观,但如果不是,现在您知道原因了:

stroke-dashoffset

stroke-dashoffset属性接受正值或负值的长度或百分比,并指定开始渲染虚线的虚线模式的距离。这个偏移量可以在以下代码示例中看到:

<svg width="400" height="300" viewBox="0 0 400 300"  
 >
 <rect x="50" y="20" width="300" height="50" fill="none"
  stroke="#000000" stroke-width="4" stroke-dasharray="10 10"></rect>
 <rect x="50" y="80" width="300" height="50" fill="none"
  stroke="#000000" stroke-width="4" stroke-dasharray="10 10" stroke- 
  dashoffset="25"></rect>
 <rect x="50" y="140" width="300" height="50" fill="none"
  stroke="#000000" stroke-width="4" stroke-dasharray="10 10" stroke-
  dashoffset="-25"></rect>
</svg>

这个属性的效果可以在以下截图中看到:

stroke-linecap

stroke-linecap属性指示在开放线的末端呈现的形状。选项包括buttroundsquareinherit。以下代码示例展示了不同的渲染选项。两条红线是为了显示buttsquare之间的区别。butt使stroke与线的末端齐平。square端延伸到线的末端,包括stroke的厚度:

<svg  width="500" height="400"
   viewBox="0 0 500 400" version="1.1"> 
 <line fill="none" stroke-width="20" stroke="#000000" x1="20" y1="100" 
    x2="450" y2="100" stroke-linecap="butt" /> 
 <line fill="none" stroke-width="20" stroke="#000000" x1="20" y1="200"
    x2="450" y2="200" stroke-linecap="round" /> 
 <line fill="none" stroke-width="20" stroke="#000000" x1="20" y1="300"
    x2="450" y2="300" stroke-linecap="square" /> 
 <line fill="none" stroke-width="2" stroke="rgba(255,0,0,1)" x1="20" 
    y1="0" x2="20" y2="400" /> 
 <line fill="none" stroke-width="2" stroke="rgba(255,0,0,1)" x1="450" 
    y1="0" x2="450" y2="400" /> 
</svg> 

这个结果可以在以下截图中看到:

stroke-linejoin

stroke-linejoin属性定义了path和基本形状的拐角的呈现方式。可能的值有miterroundbevelinheritRound呈现平滑的曲线角,miter产生只有一个角的尖边,bevel在角上添加一个新的角来创建一个复合角:

  <svg width="400" height="300" viewBox="0 0 400 300" 
    > 
  <rect x="50" y="20" width="300" height="50" fill="none" 
    stroke="#000000" stroke-width="20"  stroke-linejoin="miter"></rect> 
  <rect x="50" y="100" width="300" height="50" fill="none" 
     stroke="#000000" stroke-width="20"   stroke-linejoin="bevel">  
  </rect> 
  <rect x="50" y="180" width="300" height="50" fill="none" 
     stroke="#000000" stroke-width="20"  stroke-linejoin="round">
   </rect> 
 </svg> 

这些选项可以在以下截图中看到:

stroke-opacity

stroke-opacity属性的作用与您预期的一样。它设置了描边对象的不透明度。以下示例在三个单独的矩形上设置了三种不同的不透明度。您可以看到stroke不仅与页面的背景交互,还与矩形的填充区域交互,因为stroke位于矩形边缘的中心,并且部分覆盖了填充区域:

在 SVG 元素上没有简单的方法来更改stroke属性的定位。在图形程序中,可以将stroke属性设置为在框的内部、在框的边缘上居中(这是 SVG 的做法)和在框的外部。在新的 SVG strokes (www.w3.org/TR/svg-strokes/)规范中有一个提案来更改stroke的对齐方式(称为 stroke-alignment),但目前浏览器中还没有这样的功能。

<svg width="400" height="300" viewBox="0 0 400 300" 
  >
 <rect x="50" y="20" width="300" height="50" fill="none"
  stroke="#000000" stroke-width="20" stroke-opacity=".25"></rect>
 <rect x="50" y="100" width="300" height="50" fill="none"
  stroke="#000000" stroke-width="20" stroke-opacity=".5"></rect>
 <rect x="50" y="180" width="300" height="50" fill="none"
  stroke="#000000" stroke-width="20" stroke-opacity="1"></rect>
</svg>

前面代码的输出可以在以下截图中看到:

现在我们已经看过了stroke的不同选项,是时候看看一些其他填充选项了。这些是我们之前提到的绘图服务器元素。您已经遇到了其中之一,linearGradient。您还将了解另外两个常用的,radialGradientpattern

linearGradient 和 radialGradient

您已经在第一章中看到了linearGradient元素,介绍可伸缩矢量图形。还有radialGradient,它的工作方式基本相同,只是它呈现以中心点为中心辐射的渐变。这两个元素都添加到defs部分,每个元素都有一系列带有offsetstop-colorstop,定义了渐变。

然后,它们通过它们的id属性作为rectfill属性的参数引用:

<svg width="400" height="300" viewBox="0 0 400 300"  
 >
    <defs>
        <linearGradient id="linear">
            <stop offset="5%" stop-color="green"/>
            <stop offset="95%" stop-color="gold"/>
        </linearGradient>
        <radialGradient id="radial">
            <stop offset="10%" stop-color="gold"/>
            <stop offset="95%" stop-color="green"/>
        </radialGradient>
    </defs>
    <rect x="50" y="20" width="100" height="100" fill="url(#radial)">
    </rect>
    <rect x="200" y="20" width="100" height="100" fill="url(#linear)"> 
    </rect>
</svg>

这会产生以下输出:

pattern 元素

我们将要看的最后一个绘图服务器是pattern元素。pattern允许您定义一个小的图形元素,您可以将其引用为fillstroke并在元素上以重复的图案平铺。在这个例子中,我们使用了一个pattern元素,它有一个单独的子polygon元素,定义了两条对角线,组合在一起创建了一个长图案:

<svg width="400" height="400" viewBox="0 0 400 400" 
 >
    <pattern id="pattern-example" width="100" height="100"
      patternUnits="userSpaceOnUse">
    <polygon points="0,50 0,100 50,50 100,100 100,75 50,25 0,75" 
      fill="#000000"></polygon>
    </pattern>
    <rect x="0" y="0" width="400" height="400" fill="url(#pattern-
     example)"></rect>
</svg>

在浏览器中呈现,这会创建以下锯齿状图案:

创作程序

到目前为止,本书中的所有示例都是手工生成的。在实践中,正如您将在本书中了解到的那样,SVG 通常是由软件生成的。本书的大部分内容将涉及使用基于 Web 的工具和库创建和操作 SVG,但 SVG 图像也可以由桌面绘图应用程序生成。在 Web 上工作时,您经常会使用设计师在应用程序中创建的 SVG 图像,例如 Inkscape (inkscape.org/en/)、Adobe Illustrator (www.adobe.com/products/illustrator.html)或 Sketch (www.sketchapp.com/)。这些应用程序非常棒,因为它们允许非技术设计师使用高级绘图工具来创建 SVG 图像。

虽然这不是本书的其余部分的要求,但我建议您找到一些可以用来以这种方式编写 SVG 的工具。虽然您希望学习如何在动态的基于 Web 的环境中使用 SVG,但拥有使用高级绘图工具来更新和操作 SVG 元素的选项是很棒的。多年来,我一直在使用 Adobe Illustrator 和 Inkscape,许多人都喜欢 Sketch,所以这是三个开始的选择。对于刚开始,我建议首先看看 Inkscape。Inkscape 是一个免费的开源软件,发布在 GNU 许可下,从功能的角度来看相当不错,所以是一个很好的默认选择。

无论您选择哪个应用程序(甚至如果您不选择任何应用程序,只是继承了一个 SVG 图像),都要知道这些应用程序存在一些缺点。这些应用程序是为了创作体验而设计的,并不会生成为网络优化的 SVG 图像,因此在将由图形程序创建的 SVG 图像导入到网络项目时,要牢记这一点非常重要。您将在本书的后面学习更多关于优化 SVG 图像的知识,但是您应该从一开始就意识到您将面对的挑战。

看一下以下的屏幕截图。它显示了两个渲染完全相同图像的文件之间的差异。左边的是 Inkscape 输出的 SVG 源文件。右边的文件是经过优化的版本。正如您所看到的,Inkscape 文件中有很多额外的数据。这些数据是应用程序所需的,但在网络上并不需要,因此删除它们可以显著减小文件大小:

您将在第十一章中学习清理 SVG 文件的工具,优化 SVG 的工具

总结

在本章中,您了解了多个 SVG 功能。您了解了path,它允许您使用线条和曲线绘制复杂的形状。您还了解了一些基本绘图工具,可以用它们来绘制线条、椭圆、多边形和折线。此外,您还了解了一些描边和填充选项。

最后,您了解了使用软件绘制静态 SVG 的选项,并了解了这样做可能存在的一些缺点。

在第三章中,深入了解 SVG 创作,您将继续学习 SVG 创作,增加您已经体验过的工具列表,并允许您创建更复杂的 SVG 图像。

第三章:深入挖掘 SVG 创作

到目前为止,在这本书中,你已经接触到了大部分基本的 SVG 功能和元素。只用到目前为止你所体验过的工具,你就可以开始使用 SVG 做一些真正的任务了。也就是说,SVG 还有很多其他功能。本章将开始介绍更高级的 SVG 工具。其中一些技术将在进行动态 SVG 动画和可视化方面发挥重要作用。

本章将涵盖以下主题:

  • 转换

  • 裁剪和遮罩

  • 将内容导入 SVG

  • 滤镜效果

  • 在网络上提供 SVG

所有这些,以及你已经学到的工具,将为你打下坚实的 SVG 基础。

转换

SVG 中的变换允许你以各种方式操纵 SVG 元素,包括缩放、旋转、倾斜和平移(看起来像是移动元素,但并不完全是)。使用变换允许你操纵 SVG 而不改变其固有值(例如高度、宽度、xy),这在以动态方式操纵元素时很重要。

本节将逐一介绍常见的变换函数,并附上每个函数的示例。

平移

translate变换通过指定的xy坐标移动 SVG 元素。平移改变了元素坐标系的原点

如果没有提供y坐标,它是一个可选参数,假定与提供的x参数相等。

下面的示例显示了三个等效的圆。第一个圆没有以任何方式进行变换。第二个圆通过单个参数(10)进行了变换,它在x轴和y轴上分别移动了10。第三个在x平面上平移了"75"像素,在y平面上没有平移。在每种情况下,底层元素具有等效的度量,但它们显示方式不同。

你可能会问,为什么不直接移动元素。首先,在动态 SVG 中,这是有用的,因为如果你移动元素,你不必跟踪元素的原始位置。你可以通过移除变换来简单地将元素重置为其原始状态:

<svg  width="350" height="150"
  viewBox="0 0 350 150" version="1.1">
    <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,.5)"/>
    <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,.5)" 
     transform="translate(10)" />
    <circle cx="100" cy="75" r="50" fill="rgba(255,0,0,.5)"
     transform="translate(75,0)" />
</svg>

你可以在下面的截图中看到输出:

缩放

scale变换通过指定的xy坐标对 SVG 元素进行缩放。单位是因子,因此传入两个将会使元素的尺寸加倍

translate一样,y坐标是可选的,如果没有提供,它被假定为与提供的x参数相等。

如果你做过 CSS 变换并缩放了一个元素,你可能会对scale的工作方式感到惊讶。即使你没有做过 CSS,你也可能会感到惊讶。

在 SVG 中,缩放是从坐标系的原点开始的。请看下面的例子,显示了三个单独的方框。一个根本没有缩放。接下来的两个矩形在两个轴上都缩放了1.25倍,然后在x轴上缩放了2倍,而在y轴上没有缩放:

<svg  width="500" height="500"
 viewBox="0 0 500 500" version="1.1">
    <rect x="100" y="100" width="100" height="100" stroke="blue"
     fill="none"></rect>
    <rect x="100" y="100" width="100" height="100" stroke="red"
     fill="rgba(255,0,0,.5)" transform="scale(1.25)"></rect>
    <rect x="100" y="100" width="100" height="100" stroke="red" 
     fill="rgba(255,0,0,.5)" transform="scale(2,1)"></rect>
</svg>

正如你在下面的截图中所看到的,结果是元素的尺寸不仅被缩放,而且与坐标系原点的距离也被缩放了。第一个元素在两个方向上都进行了调整,沿着xy平面。第二个元素沿着x轴向右移动:

与下面的代码相比,显示了 CSS 缩放的工作方式。在 CSS 中使用相同的缩放因子会产生完全不同的结果。CSS 不是从 SVG 文档的原点进行缩放,而是从元素本身的中心点进行缩放。语法可能看起来相似,但结果是不同的:

<head>
<style type="text/css">
    div {
        position: absolute;
        left: 100px;
        top: 100px;
        width: 100px;
        height: 100px;
        border: 1px solid blue;
    }
    .scale-1-25 {
        transform: scale(1.25);
        border: 1px solid red;
        background: rgba(255,0,0,.5);
    }
    .scale-2-by-1 {
        transform: scale(2,1);
        border: 1px solid red;
        background: rgba(255,0,0,.5);
    }
</style>
</head>
<body>
    <div></div>
    <div class="scale-1-25"></div>
    <div class="scale-2-by-1"></div>
</body>

结果可以在下面的截图中看到:

如果您想要在 SVG 中产生类似的效果,有一个有趣的技巧可以使用。这个技巧也可以用来说明transform属性可以接受多个变换函数。您不仅限于一个。

那么,它是如何工作的呢?正如我所提到的,CSS 变换的原点是被变换的盒子的中心。这种技术在 SVG 中复制了相同的原点。

要做到这一点,您需要做一个技巧,即将元素的原点移动到一个新的原点,使其与 CSS 原点匹配。这是它的工作原理。在这种情况下,我们的矩形在坐标系中的位置是(100, 100),边长为100像素。因此,盒子的中心点位于(150, 150)。通过将元素平移(150,150),将这些元素的原点设置为等同于 CSS 原点的位置。请记住,CSS 原点是盒子的中心点(在变换之前是(150,150)),平移元素实际上改变了它的原点。

在平移之后,我们应用了缩放。这发生在新的原点(150,150)处(再次等同于 CSS 原点),并将正方形分别扩大了1.252。最后,我们将元素返回到其原始原点(0,0),因为它们是在 CSS 等效原点(150,150)处进行操作的,所以缩放后的元素现在被适当地居中了:

<svg  width="500" height="500"
  viewBox="0 0 500 500" version="1.1">
    <rect x="100" y="100" width="100" height="100" stroke="red"
     fill="rgba(255,0,0,.5)"></rect>
    <rect x="100" y="100" width="100" height="100" stroke="red" 
     fill="rgba(255,0,0,.5)" transform="translate(150 150) scale(1.25)
      translate(-150 -150)"></rect>
    <rect x="100" y="100" width="100" height="100" stroke="red" 
     fill="rgba(255,0,0,.5)" transform="translate(150 150) scale(2,1) 
     translate(-150 -150)"></rect>
</svg>

以下插图逐步展示了这是如何工作的:

  1. 第一帧显示了起始位置。100像素的矩形放置在(100,100),它们的原点是(0,0)

  2. 然后它们被平移(150,150)

  3. 然后,它们从新的原点(150,150)处进行了变换,分别为1.25(2,1)

  4. 它们被平移到(0,0),同时保持新的缩放。此时它们的实际原点是(0,0),但它呈现出来好像是 CSS 原点(150,150)

旋转

rotate变换通过一定角度旋转元素。这个变换有三个参数。第一个是角度数。第二个和第三个参数是定义旋转原点的xy坐标。如果元素没有旋转原点,则使用视口的原点。这可以在以下两个代码示例中看到,其中在 SVG 元素上绘制了九个矩形。第一个没有被变换。接下来的八个依次旋转了十度:

<svg  width="700" height="700" 
  viewBox="0 0 700 700" version="1.1">
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)"/>
    <rect x="600" y="0" width="100" height="100" 
      fill="rgba(255,0,0,.5)" transform="rotate(10)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(20)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(30)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(40)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(50)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(60)"/>
    <rect x="600" y="0" width="100" height="100"
      fill="rgba(255,0,0,.5)" transform="rotate(70)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(80)"/>
</svg>

如您在渲染代码的下面截图中所见,它们在整个画布上arc,并且视口的(0,0)点位于旋转的原点:

与之相比,下面的代码将旋转点更改为视口的中心点,以及x轴上的顶部和y轴上的顶部:

<svg  width="700" height="700" viewBox="0 0 700 700" version="1.1">
    <rect x="600" y="0" width="100" height="100" 
      fill="rgba(255,0,0,.5)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(10 350 0)"/>
    <rect x="600" y="0" width="100" height="100"  
     fill="rgba(255,0,0,.5)" transform="rotate(20 350 0)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(30 350 0)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(40 350 0)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(50 350 0)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(60 350 0)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(70 350 0)"/>
    <rect x="600" y="0" width="100" height="100" 
     fill="rgba(255,0,0,.5)" transform="rotate(80 350 0)"/>
</svg>

如您所见,当这段代码在浏览器中渲染时,相同角度的旋转arc在视口的右上角四分之一中。正方形从新的原点辐射出来

与缩放一样,如果您想要围绕元素的中心点旋转,您可以使用在该部分学到的相同平移技巧。在下面的代码示例中,矩形被平移了相当于它们的中心点(100,100),旋转了10度,然后又被平移到了它们的原始原点:

<svg  width="400" height="400" viewBox="0 0 200 200" version="1.1">
    <rect x="50" y="50" width="100" height="100" 
    fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(10) translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100" 
    fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(20) 
    translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100" 
    fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(30)
     translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100" 
    fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(40) 
    translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100"
     fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(50) 
     translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100" 
     fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(60)
     translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100"
     fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(70) 
      translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100" 
    fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(80)
     translate(-100,-100)"/>
    <rect x="50" y="50" width="100" height="100" 
    fill="rgba(255,0,0,.2)" transform="translate(100,100) rotate(90) 
    translate(-100,-100)"/>
</svg>

这产生了以下输出:

倾斜

skew变换通过指定的轴沿角度倾斜元素。与rotatescale一样,skew基于当前原点。以下代码示例显示了两组元素。一组沿着x轴倾斜,另一组沿着y轴倾斜。对于每组元素,有一个变换专注于skew,然后还有另一个相同量的skew变换,也包括平移技术:

<svg  width="500" height="500" viewBox="0 0 500 500" version="1.1">
    <rect x="100" y="100" width="100" height="100"
     fill="rgba(255,0,0,.1)" transform="skewX(10)"/>
    <rect x="100" y="100" width="100" height="100" stroke="blue"
     fill="none"/>
    <rect x="100" y="100" width="100" height="100" 
    fill="rgba(0,255,0,.1)" transform="translate(150,150) skewX(10) 
    translate(-150,-150)"/>
    <rect x="300" y="300" width="100" height="100" stroke="blue" 
    fill="none"/>
    <rect x="300" y="300" width="100" height="100" 
     fill="rgba(255,0,0,.1)" transform="skewY(10)"/>
    <rect x="300" y="300" width="100" height="100" 
    fill="rgba(0,255,0,.1)" transform="translate(300,300) skewY(10)
     translate(-300,-300)"/>
</svg>

你可以在以下截图中看到此代码的输出。蓝色正方形显示了原始位置,然后两个倾斜的元素排列在其上,以显示基于原始原点的倾斜和使用平移技术改变原点到元素中心的差异:

还有另一种选项可以变换元素。你可以使用所谓的变换矩阵。矩阵变换很强大(它们可以表示任何其他变换函数),但也很复杂,它们严重依赖数学。由于并非每个人都认为数学很有趣,矩阵变换并不像其他变换函数那样常见。因此,我不打算在这里涵盖它们。实际上,你可以用已经学到的方法做任何你需要做的事情。

裁剪和遮罩

裁剪和遮罩允许你在 SVG 文档中减去元素的部分。

剪切路径,使用clipPath元素实现,可以使用路径、文本元素和基本形状的任意组合作为简单蒙版的轮廓。这意味着clipPath元素轮廓内部的所有内容都是可见的,而外部的所有内容都被裁剪掉。clipPath中的每个像素要么是打开的,要么是关闭的。

遮罩,使用mask元素实现,可以包含图形、文本和基本形状,作为半透明的遮罩。使用遮罩,每个像素值表示不透明度的程度,可以从完全透明到完全不透明。

裁剪

SVG 中的clipPath元素允许你从另一个形状中裁剪出一个形状。裁剪使用形状的几何图形来定义被裁剪的区域。它不考虑除形状之外的任何东西,因此strokefill等属性不会改变被裁剪的区域。

以下代码示例显示了一个非常简单但非常有用的clipPath元素的使用模式。基本效果是切掉一个复杂元素的一半(我们在第二章中绘制的星星,开始使用 SVG 进行创作),以便将其放在另一个相同星星的实例上,创建一个红色和黑色的分割星星设计。虽然你可以创建两个星星的一半并将它们放在一起,但混合和匹配相同元素的实例更加灵活。

让我们看看这是如何工作的。

首先,在defs部分,我们创建clipPath元素本身。clipPath的任何子元素都将捆绑在一起,以创建稍后将使用的裁剪模式。在这种情况下,它是一个简单的矩形,覆盖了画布的一半。它的 ID 是"box"。接下来,我们创建了一个星星的可重用实例,我们在第二章中创建了它,开始使用 SVG 进行创作。我们给它一个 ID 为"star"。在defs部分之外,我们把它全部放在一起。使用两个use元素的实例,它允许你交换在其他地方定义的元素,我们链接到星星的polygon,并将其两次插入文档中,一次填充为红色,一次填充为黑色。请注意,用户元素使用片段标识符来引用多边形。"#star"是一个有效的相对 URL,指向本页上特定id。第二个变体具有一个clip-path属性,它链接到我们的boxclipPath

<svg  width="240" height="240" viewBox="0 0 240 240" version="1.1">
    <defs>
        <clipPath id="box" maskUnits="userSpaceOnUse" x="0" y="0"
         width="240" height="240">
            <rect x="120" y="0" width="240" height="240" fill="red" >
            </rect>
        </clipPath>
        <polygon id="star" points="95,95 120,5 150,95 235,95 165,150 
           195,235 120,180 50,235 75,150 5,95"></polygon>
    </defs>
    <use href="#star" fill="red"></use>
    <use href="#star" fill="black" clip-path="url(#box)"></use>
</svg>

该代码的输出可以在以下截图中看到。红色的星星实例暴露为黑色星星的左半部分,该部分被clipPath元素中定义的正方形剪切掉:

遮罩

遮罩与裁剪相反,考虑了除了元素简单形状之外的属性。正如前面提到的,您可以利用全透明、半透明或完全不透明的像素。这可以产生有趣的效果。

以下示例显示了如何一起使用多个遮罩。在此示例中,我们大量使用defs部分,然后使用不同的可重用元素组合图像。

首先,我们创建两个渐变。一个是线性渐变,有五个步骤,大部分是黑色,中间创建了一个非常强烈的白色带。第二个是径向渐变,中心区域是黑色,周围是一个非常大的白色圆圈。将这些用作遮罩意味着这些渐变中的每个像素都落在从完全不透明(黑色像素)到完全透明(白色像素)和中间可变透明度的连续范围上。

单独看这些渐变:

然后,我们创建一个写着“精通 SVG”的text元素,并引入一个pattern元素,您将从第二章中认识到它,开始使用 SVG 进行创作

在 SVG 元素的主体中,我们链接到文本元素,使用片段标识符(#mastering-SVG)指向defs部分中text元素的 ID,并使用mask属性将两个遮罩应用于它们,mask属性的url值指向mask属性的片段标识符。单独看看这些遮罩如何影响文本元素:

将所有内容放在一起,我们将两个文本元素叠放在一起,并在文本框后面添加一个带图案的框:

<svg  width="500" height="120" viewBox="0 0 500 120" version="1.1">
    <defs>
        <linearGradient id="gradient">
            <stop offset="0" stop-color="black" stop-opacity="1" />
            <stop offset=".25" stop-color="black" stop-opacity="1" />
            <stop offset=".5" stop-color="white" stop-opacity="1" />
            <stop offset=".75" stop-color="black" stop-opacity="1" />
            <stop offset="1" stop-color="black" stop-opacity="1" />
        </linearGradient>
        <radialGradient id="highlight-gradient">
            <stop offset=".25" stop-color="black" stop-opacity="1" />
            <stop offset=".75" stop-color="white" stop-opacity="1" />
        </radialGradient>
        <mask id="gradient-mask" maskUnits="userSpaceOnUse" x="0" y="0"
         width="500" height="240">
            <rect y="0" x="0" width="500" height="120" 
             fill="url(#gradient)"></rect>
        </mask>
        <mask id="highlight-mask" maskUnits="userSpaceOnUse" x="0" 
         y="0" width="500" height="240">
            <rect y="0" x="0" width="500" height="120" 
             fill="url(#highlight-gradient)"></rect>
        </mask>
        <text id="mastering-SVG" x="10" y="75" font-size="72" text-
         anchor="left" font-weight="bold">
            Mastering SVG
        </text>
        <pattern id="pattern-example" width="100" height="100" 
         patternUnits="userSpaceOnUse">
            <rect width="100" height="100" fill="darkred" x="0" y="0">
            </rect>
            <polygon points="0,50 0,100 50,50 100,100 100,75 50,25 
              0,75" fill="rgb(83,1,1)">               
            </polygon>
        </pattern>
    </defs>
    <rect x="0" y="0" width="500" height="120" fill="url(#pattern-
      example)"></rect>
    <use href="#mastering-SVG" fill="gold" mask="url(#gradient-mask)" 
     x="120"></use>
    <use href="#mastering-SVG" fill="red" mask="url(#highlight-mask)"></use>
</svg>

在浏览器中运行后,我们得到以下输出。您可以看到两个文本元素中可见的黄色区域和红色区域混合在一起。中心和边缘有完全不透明的颜色区域,与半透明颜色区域混合,其中背景图案透过,位于两者之间:

本节只是简单介绍了遮罩和裁剪的可能性。在本书的其余部分中,您将继续看到这些强大技术的示例。

将图像导入 SVG

除了在 SVG 中批量创建图像之外,还可以将其他图像引入 SVG 文档中。

有几种方法可以做到这一点。一种方法是使用 SVGimage元素,并以一种您熟悉的方式导入图像,如果您使用过 HTMLimg元素,这种方式对您来说将是熟悉的。在此示例中,我们使用image元素。它采用href属性,类似于 HTML 中的img src,并且具有heightwidth属性。与 HTMLimg元素不同,它还接受xy位置:

在 HTML 文档的上下文中,HTMLspec实际上将IMAGE定义为img的同义词。它只存在于内联 SVG 的上下文中。

<svg  width="1000" height="485" viewBox="0 0 1000 485" version="1.1">
    <image href="take-2-central-2017.jpg" width="1000" height="485" 
      x="0" y="0" ></image>
    <text x="300" y="400" fill="white" font-family="verdana, helvetica" 
     font-size="36" text-anchor="left">
        REACT @ Central Square 2017
    </text>
</svg>

在浏览器中呈现,我们得到了完整的照片图像,SVG 文本元素作为标题:

你也可以使用image元素来导入其他 SVG 图像。该技术有一定的限制,限制了导入的 SVG 元素的实用性。它们基本上被视为静态图像,因此诸如进一步导入图像之类的事情是行不通的;你不能在导入的 SVG 图像内导入其他图像。只有第一个引用的图像会被导入。要使用导入的 SVG 图像的全部功能,你应该使用use元素并指向外部 URL。通过这种技术,你还可以针对导入文档的特定片段。这种技术可以让你创建一个符号库,并通过引用将符号导入到你的 SVG 文档中。

在这个简单的例子中,我们展示了如何使用use元素并引用包含文档的片段来正确导入图像。#image指向svg-with-import.svg中特定元素的id元素:

<svg  width="1000" height="970" viewBox="0 0 1000 970" version="1.1">
<image href="svg-with-import.svg" width="1000" height="485" x="0" y="0"></image>
<use xlink:href="svg-with-import.svg#image" width="1000" height="485" x="0" y="485"></use>
</svg>

这个文档顶部的空白处显示了图像加载失败的位置:

要使这个示例在低于 8 版本的 Internet Explorer 中工作,你需要使用一个叫做svg4everybody的 polyfill 脚本(github.com/jonathantneal/svg4everybody)。将它插入到你的文档中,在需要使用 SVG 时调用它,它就可以工作。svg4everybody还可以在 Safari 6 和 Edge 12 中填充体验。如何修复你的页面在下面的代码示例中显示。你包含文件,然后调用svg4everybody()脚本:

<script src="img/svg4everybody.min.js"></script>
<script>svg4everybody();</script>

滤镜

滤镜允许你对元素或元素组应用各种效果。滤镜允许你模糊图像,应用照明效果,以及许多其他高级图像处理技术。如果你曾经使用过 Adobe Photoshop 或其他图形处理程序,这些滤镜就像你在那个环境中看到的滤镜一样。

滤镜是在 SVG 文档的defs部分中实现的,并作为filter元素的一部分进行分组。它们的引用方式与maskclipPath元素相同,通过片段 URL。以下示例显示了应用于圆的常见高斯模糊滤镜:

<svg
 width="300" height="150" viewBox="0 0 300 150">
    <filter id="blurIsm">
        <feGaussianBlur in="SourceGraphic" stdDeviation="5"/>
    </filter>
    <circle cx="75" cy="75" r="50" fill="red"/>
    <circle cx="200" cy="75" r="50" fill="red" filter="url(#blurIsm)"/>
</svg>

在浏览器中呈现,你可以看到右侧的模糊圆圈:

我不打算在本书中详细介绍滤镜。有很多滤镜;浏览器支持的级别有所不同(有时令人困惑),并且解释起来可能非常复杂。我想向你展示一个,这样你就可以看到基本模式,这个是最简单的。所有其他滤镜都遵循相同的一般模式。一个filter或一系列滤镜被分组在defs部分,并通过id元素进行引用。只要知道这个简单的模式,你就可以准备好尝试它们,或者将它们纳入你的项目中。

在网络上提供 SVG

在我们进入更多关于 SVG 与 web 技术更广泛的交互方式的细节章节之前,关于 SVG 的最后一点说明:如果你要在网络上提供 SVG,你需要确保它以正确的内容类型提供。浏览器期望以"image/svg+xml"媒体类型提供 SVG。如果你遇到 SVG 图像不显示的问题,并且你可以验证它们存在于服务器上,最好检查头部(使用你选择的浏览器调试器的网络选项卡)以查看它们是否被正确提供。如果没有(例如,如果它们是text/xml),那么你需要正确设置媒体类型。本节概述了如何在常见的 web 服务器中设置正确的媒体类型。

Apache

在 Apache 中添加正确的媒体类型就像在你的.htaccess文件中添加以下行一样简单:

AddType image/svg+xml svg svgz

nginx

在 nginx 中添加正确的媒体类型需要你在你的mime.types文件中有以下条目:

types {
    image/svg+xml svg svgz;
}

IIS

在 IIS 中添加正确的媒体类型有两种方式。您可以使用 IIS 管理器(docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc753281(v=ws.10))添加媒体类型,或者您可以将以下条目添加到web.config中:

<configuration>
    <system.webServer>
        <staticContent>
            <remove fileExtension=".svg"/>
            <mimeMap fileExtension=".svg" mimeType="image/svg+xml"/>
            <remove fileExtension=".svgz"/>
            <mimeMap fileExtension=".svgz" mimeType="image/svg+xml"/>
        </staticContent>
    </system.webServer>
</configuration>

总结

在本章中,您了解了许多更高级的 SVG 功能。您了解了多重变换,这使您能够在不改变 SVG 元素的基本结构的情况下操作它们。这为我们在本书中将继续探讨的许多可能性打开了大门。

您还了解了裁剪和遮罩,这使您能够通过复杂的图形减去图像的部分。这包括使用可变不透明度来操作图像的能力。

此外,您还了解了实现基本 SVG 滤镜以及在常见 Web 服务器上提供 SVG 文件的方法。

在第四章中,在 HTML 中使用 SVG,您将了解有关在 HTML 文档中使用 SVG 的一些细节,这是 SVG 真正展现其力量供全世界看到的地方。

第四章:在 HTML 中使用 SVG

到目前为止,在本书中,您已经接触到了 SVG 的基本构建块:在 SVG 规范中定义的功能和功能。虽然 SVG 可以独立存在,但当它在现代网络上得到应用时,它真正发挥作用。现在的网络是多设备、多形态和多连接速度的环境,SVG 有助于解决现代网络开发人员面临的许多棘手问题。因此,接下来的几章将重点介绍 SVG 与其他核心技术的集成:HTML、CSS 和 JavaScript。本章非常直接,重点是在 HTML 文档的上下文中使用 SVG。网络上的一切都始于 HTML,因此确保您的 SVG 在 HTML 中正常运行是正确的方法。

您已经学习了如何将 SVG 插入 HTML 文档中作为图像或内联 SVG 元素的基础知识,可以在第一章 介绍可伸缩矢量图形中找到。本章将在此基础上添加一些细节。

在本章中,您将学习以下内容:

  • SVG 和可访问性

  • 使用 SVG 图像进行响应式网页设计以及作为响应式图像解决方案的好处

  • 在 HTML 文档的上下文中使用内联 SVG 的工作细节

那么,让我们开始吧!

SVG、HTML 和可访问性

网络可访问性旨在确保残障人士可以访问网站和应用程序。总体目标是提供以这样一种方式提供内容,以便残障用户可以直接访问,或者如果由于其残障(例如,听障用户需要音频内容),无法直接访问,则提供结构良好的替代内容,以传达相同的信息。然后,可以通过辅助技术AT)访问这些结构良好的替代内容。辅助技术的最常见示例是屏幕阅读器

所有平台都有屏幕阅读器。您可以使用一些免费应用程序进行测试,包括以下内容:

  • NVDA(Windows)

  • Apple VoiceOver(OS X)

  • Orca(Linux)

对于 SVG 这种视觉格式,重点是在适当的情况下提供描述图像的文本内容。

正如您可能知道的那样,HTML 本身具有辅助功能的工具和最佳实践。除了 HTML 中的工具外,还有一组名为可访问丰富互联网应用ARIA)的技术,它定义了使网络和网络应用对残障人士更具可访问性的方法。 ARIA 提供了一组特殊的辅助功能属性,当添加到 HTML 中时,可以提供有关页面或应用程序的辅助功能信息。例如,role属性定义了元素的类型(文章、菜单或图像)。

正如您在第一章 介绍可伸缩矢量图形中看到的,将 SVG 插入 HTML 文档的两种常见方法是作为图像的src和作为内联 SVG 元素(或元素)。本节将添加一些关于使用 SVG、HTML 和 ARIA 属性的注意事项,以确保您的内容在使用这两种技术时仍然具有可访问性。

SVG 作为图像的 src

将 SVG 放入文档的最简单方法是作为img元素的src。正如您在第一章 介绍可伸缩矢量图形中看到的那样,这样做就像引用*.svg元素一样简单,就像在img元素的src属性上引用任何图像一样。

至于可访问性,如果您遵循有关可访问性和图像的最佳实践,您可以继续对 SVG 图像执行相同的操作。alt 属性应该存在,并且如果辅助技术需要,它应该正确描述内容。您可能会想知道为什么您需要这样做,尤其是对于已经在其源中具有描述性文本的 SVG 图像。请注意,SVG 文件中的任何文本内容实际上被屏幕阅读器锁定,因此即使您使用 SVG,作为一种具有描述性的基于标记的图像格式,它在这种情况下至少表现得就像一个常见的位图文件格式。

除了替代文本之外,旧版 Safari(早于 Safari 桌面 9.1.1 版或 iOS 上的 9.3.2 版)有一个小问题需要考虑。在这些旧版本中,除非在 img 元素上设置了 role="img" ARIA 角色,否则 VoiceOver,苹果屏幕阅读器,将不会读取 alt 文本:

<img src="img/apple.svg" width="300" height="300" alt="an apple" 
 role="img">

内联 SVG

内联 SVG 为可访问性提供了更广泛的选择。例如,与我们刚讨论的 SVG 作为 img src 的情况不同,如果 SVG 中有一个或多个 text 元素,则该文本可以直接被屏幕阅读器读取。如果文本对图像有适当的描述,那么您已经提供了一个可访问的图像。您无需做其他任何事情。

如果 SVG 中的文本对图像没有描述性,或者图像没有文本,那么您可以利用两个 SVG 元素,即 title 和 desc,提供可访问的文本。这些元素与 aria-labelledby 属性结合使用,提供了一种两级的可访问性方法。以下代码示例显示了这种工作方式的基本原理。图像本身是一个苹果的插图。在浏览器中呈现时,看起来像这样:

标记如下。

SVG 元素本身有两个重要的属性。它具有一个 role 为 img 的属性,表示该元素被标识为图形。它还利用了 aria-labelledby 属性,该属性引用了两个单独的 ID,即"apple-title"和"apple-desc"。aria-labelledby 属性在元素本身和其他标签它的元素之间创建了一个关系。

我们首先遇到这两个元素中的第一个元素,即 SVG 元素的第一个子元素,即 title 元素。title 元素可用于为 SVG 元素提供元素的文本描述。它不会直接由浏览器呈现,但应该由屏幕阅读器读取,并且可以呈现为工具提示,就像 alt 属性中的文本在某些浏览器中显示的方式一样。它的 id 是"apple-title"。接下来是可选的 desc 元素。desc 允许您提供图像的更长文本描述。它的 id 是"apple-desc"。它也可以被屏幕阅读器读取,并且不会直接呈现在浏览器中。

有趣的标记的最后一部分是 role="presentation",它应用于每个子 path 元素。这样做可以将这些元素从可访问性树中排除出去,因此从可访问性的角度来看,SVG 元素被视为一个图形:

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>Mastering SVG- Accessible Inline SVG </title>
</head>
<body>
 <svg  width="300" height="300"
  viewBox="0 0 300 300" version="1.1" role="img" aria-
  labelledby="apple-title apple-desc">
 <title id="apple-title">An Apple</title>
 <desc id="apple-desc">An apple, the sweet, pomaceous fruit, of the
  apple tree.</desc>
 <path role="presentation" style="fill:#784421;stroke:none;stroke-
  width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-
  opacity:1"
  d="m 105.75769,18.053573 c 0,0 46.1131,23.434525 34.01786,50.64881  
  -12.09524,27.214284 15.875,-6.803573 15.875,-6.803573 0,0
   9.07143,-23.434524 -1.5119,-38.55357 -10.58334,-15.1190474 
  -48.38096,-5.291667 -48.38096,-5.291667 z"
 />
 <path role="presentation" style="fill:#ff0000;stroke:none;stroke-
  width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-
  opacity:1"
  d="m 146.65476,61.898812 c 0,0 -139.0952362,-66.5238097
 -127.755951,73.327378 11.339285,139.85118 92.218641,132.57921
  123.220231,124.73214 23.47822,-5.94277 108.10119,52.16071 
  127.00001,-111.125 C 286.06755,2.3982366 146.65476,61.898812 
  146.65476,61.898812 Z"
 />

 <path role="presentation" style="fill:#ffffff;stroke:none;stroke-
  width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-
  opacity:1"
  d="m 183.69643,64.92262 c 0,0 50.21311,5.546816 41.57738,74.83928 
  -2.32319,18.64109 31.75,-34.7738 21.92261,-54.428565 C
  237.36905,65.678572 219.22619,55.851191 183.69643,64.92262 Z"
 />
 </svg>
</body>
</html>

本节描述了静态 SVG 图像的可访问性。动态 SVG 还有其他可行的可访问性技术,包括其他 ARIA 属性,例如 ARIA-live 区域。在适用的情况下,您将在以下章节中了解这些内容。也就是说,正确掌握静态 SVG 的基础知识是一个很好的开始,学会使用屏幕阅读器测试 SVG 将使您走上正确的道路。

SVG 和响应式网页设计

响应式网页设计RWD)是一种开发网站和应用程序的技术,利用流体布局网格和 CSS3 媒体查询(www.w3.org/TR/css3-mediaqueries/)来创建可以适应和响应设备或用户代理特征的布局,从而可以伸缩以呈现适用于各种屏幕尺寸的布局,而无需事先了解设备特征。

当 RWD 开始流行起来时,一个迅速浮出水面的问题是难以根据影响最终用户体验的多种变量之一(屏幕分辨率、像素深度和可用带宽)来提供正确大小的图像(文件大小和尺寸)。用户体验。屏幕分辨率、像素深度和可用带宽都结合在一起,使得为用户提供何种大小的图像成为一个复杂的问题。

接下来是多年的追求标记模式,以创建响应式内容图像。内容图像是使用img标签提供的图像,旨在作为内容呈现。这与仅用于设计的图像相比,后者可以并且应该已经用 CSS 处理。由于媒体查询得到了强有力的支持,CSS 已经提供了许多工具,可以根据多种因素呈现正确的图像。

响应式图像的一些要求如下:

  • 尽可能使用最小的文件大小:这实际上是核心问题。它以许多方式表现出来。在理想的世界中,我们只会发送渲染图像所需的最小字节数,以达到可接受的质量水平。

  • 利用浏览器预加载程序:所有现代 Web 浏览器都使用一种技术,其中浏览器会跳过,同时阅读文档并构建 DOM,并阅读文档,寻找可以开始下载的其他资产。

  • 为多个分辨率提供正确大小的图像:如果您要为2048像素的显示器提供大图像,则希望它是1600像素或更大的大图像。另一方面,平板电脑或手机上的大图像可能只需要320480像素宽。在这种情况下发送正确数量的数据可以显著提高性能。

  • 为多个像素比设备提供正确的图像:为了在具有高设备像素比的设备上产生清晰的图像,您需要发送比例更大的文件,这些文件将显示给定 CSS 像素的图像。在标准桌面显示器上清晰的图像会在高像素密度显示器上显示出瑕疵。显然,您可以向所有浏览器发送更高分辨率的图像,但这些像素需要带宽,因此最好只向正确的设备发送正确的图像。

  • 选择不同尺寸的图像或完全不同的图像在不同的断点:希望能够在不同的方向和屏幕分辨率下显示不同的图像。在大屏幕上,描述亚利桑那州图森的植物的文章中,您可能会使用一个宽图像,显示那里可以找到的各种耐旱植物。在纵向的小屏幕上,由于只显示一英寸高且细节很少,多样性的影响会消失,具有明显垂直宽高比的仙人掌图像可能是更好的选择。

  • 使用设计断点:围绕媒体查询断点的概念进行了大量开发。它们是 RWD 核心技术之一。图像需要与响应式网站中发生的所有其他设计更改一起进行控制。

那次探索得出的多种解决方案(picture元素和srcsetsizes属性)非常强大。花了一些时间(几年时间和大量的互联网烦恼),但最终,我们在浏览器中获得了为我们提供正确图像、正确文件大小和正确尺寸的一切所需的东西。

这不容易。这是一个复杂的问题,因为它有一个复杂的解决方案。编码是复杂的,理解起来也很复杂,需要生成每个要在网页上呈现的图像的多个版本。

让我们看看新的解决方案是如何工作的,然后我们将看看 SVG(如果由于图像要求而可用)如何使它变得不那么复杂。

srcset 属性

srcset属性是添加到img元素的新属性。您可以将其与新的picture元素一起使用,我们稍后会这样做。现在,让我们单独看一下。就像我说的,这些东西很复杂,所以值得花时间慢慢建立。

与标准的src属性一样,srcset属性告诉浏览器从哪里获取要用于img元素内容的文件。但与src引用的单个图像不同,srcset属性呈现了一个逗号分隔的 URL 列表。srcset属性还提供了关于图像大小或像素密度的提示

让我们看一个例子来理解这些提示是如何工作的。

在以下示例中,srcset属性提示有关设备像素比。在这种情况下,有两个选项。第一个选项是more-colors-small.jpg,宽度为600*350(600 乘以 350)像素,用于显示标准分辨率。第二个图像more-colors-large.jpg,宽度为1200*700像素,用于更高分辨率的显示。它仍然以600*350 CSS像素显示,但它有足够的额外图像信息,可以在更高像素密度的显示器上显示得更清晰。

src属性作为不支持srcset的浏览器的后备:

<!DOCTYPE html>
   <html lang="en">
     <head>
       <meta charset="utf-8">
     </head>
     <body>
       <img
         srcset="more-colors-small.jpg 1x,
                more-colors-large.jpg 2x"
         src="img/more-colors-small.jpg"
         alt="Many colors!"
         width="600" height="350">
     </body>
 </html>

这是设备像素比用例的解决方案。

对于每个支持图像的浏览器,src作为后备,对于不支持图像的浏览器,alt属性作为后备,这是一个很好的向后兼容的解决方案。

srcset 和 sizes 属性

为了解决更复杂的用例,srcset属性可以与新的sizes属性一起使用,使用媒体查询来提供不同的图像源,根据浏览器窗口显示不同的相对尺寸。代码示例说明了这是如何工作的。

在这个例子中,元素以src属性作为不支持图像的浏览器的后备。在这种情况下,我选择了一个较小的图像,以确保无论设备或浏览器如何,它都能快速加载。接下来是新的sizes属性。sizes接受媒体查询/图像大小对(或对列表)。

以下图表分解了组件。第一部分是媒体查询。如果您在 CSS 中使用过媒体查询,这个媒体查询应该很熟悉。如果查询为true,则图像大小设置为60vw视口宽度vw)的 60%)。如果媒体查询失败,则大小回退到100vw的默认大小:

可以有任意数量的媒体查询/大小对。第一个匹配的媒体查询获胜,如果没有匹配,则使用回退值。

这里的srcset属性更加广泛。列表中有一系列宽度在200像素到1600像素之间的图像。源集中值对的第二部分,而不是指示首选像素密度,提示浏览器图像的像素宽度(200w,400w 等)。浏览器可以根据不同的尺寸和像素密度混合和匹配最佳像素宽度与适当尺寸:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG - the srcset and sizes attributes </title>
</head>
<body>
    <img src="img/more-colors-400.jpg"
      alt="Many colors!"
      sizes="(min-width: 1024px) 60vw, 100vw"
      srcset="more-colors-200.jpg 200w,
              more-colors-400.jpg 400w,
              more-colors-600.jpg 600w,
              more-colors-1200.jpg 1200w,
              more-colors-1600.jpg 1600w">
  </body>
</html>

size的长度部分可以用任何有效的 CSS 长度来指定,这增加了这个属性的可能性和复杂性。本章将坚持使用vw度量。

图片元素

在最初的概念中,picture被设计为img元素的并行元素,模仿 HTML5 的videoaudio元素的语法。其想法是有一个picture元素包裹一系列source元素,这些元素将代表图像源的选项。它会包裹一个默认的img元素,供不支持的浏览器使用。每个源上的media属性将提示浏览器使用正确的源:

<picture alt="original proposal">
 <source src="img/high-resolution.png" media="min-width:1024px">
 <source src="img/low-resolution.png">
 <img src="img/low-resolution.png" alt="fallback image">
 </picture>

出于各种实施相关的原因,这个最初的提案被否决了。srcset填补了一些空白,但由于它没有解决所有响应式图像使用案例,规范景观中总是有一个空白。

多年过去了,最终,经过多次失败的尝试,picture被重新设计和重塑,以填补那个空白。

然而,现在,picture不再是img的替代品,而是img元素的增强,以帮助浏览器找出图像源的最佳解决方案。

让我们看一个例子。虽然srcset的例子使用了同一图像的不同分辨率版本,但这个picture的例子旨在为不同分辨率提供不同的图像。在较大的浏览器窗口中,将显示一个宽度大于高度的图像:

在小于 1,024 像素的浏览器窗口中,将使用一个正方形图像:

这个标记相对复杂,需要一些解释。

head中,注意到picturefill.min.js文件的存在。Picturefill (github.com/scottjehl/picturefill)是picture元素的 Polyfill (remysharp.com/2010/10/08/what-is-a-polyfill),为不支持的浏览器提供了 JavaScript 驱动的图片元素支持。

在 HTML 文档的主体中,picture元素包裹整个解决方案。它让浏览器知道它应该使用这个picture元素来为子img元素找到正确的源。然而,我们并不立即进入img元素。我们遇到的第一个子元素是source元素。

从开发者的角度来看,source的工作方式与最初的提案意图相同。如果媒体查询匹配,就使用该source。如果不匹配,则转到堆栈中的下一个媒体查询。

在这里,您有一个媒体查询,寻找最小宽度为1024像素的页面。如果媒体查询匹配,srcset属性用于让浏览器在600像素到1600像素宽的三个不同源图像中进行选择。由于此图像旨在以50vw显示,这将为大多数显示器提供良好的覆盖范围。接下来是备用的img元素,它也包含一个srcset。如果浏览器不支持picturesource,或者前面的媒体查询不匹配,您可以在这里使用srcset属性来获取此图像的源。sizes属性允许您进一步调整小于1024像素的范围的显示:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG- picture element </title>
  <script src="img/picturefill.min.js"></script>
</head>
<body>
    <picture>
     <source
      media="(min-width: 1024px)"
      sizes="50vw"
      srcset="more-colors-600.jpg 600w,
        more-colors-1200.jpg 1200w,
        more-colors-1600.jpg 1600w">
       <img src="img/more-colors-square-400.jpg"
         alt="Many colors!" 
         sizes="(min-width: 768px) 60vw, 100vw"
         srcset="more-colors-square-200.jpg 200w,
          more-colors-square-400.jpg 400w,
          more-colors-square-600.jpg 800w,
          more-colors-square-800.jpg 1200w">
      </picture>
  </body>
</html>

虽然复杂,这种picture模式解决了不同图像尺寸和不同格式的分离艺术指导选择的问题。既然(冗长的)解释已经结束,让我们看看 SVG 如何以更少的标记解决同样的问题。您已经在第一章中看到了介绍可缩放矢量图形,SVG 图像作为img元素的src进行缩放。实际上,使用 SVG,响应式图像就像以下代码一样简单:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG- 100% image element </title>
</head>
<body>
    <img src="img/more-colors.svg" width="100%">
  </body>
</html>

它允许您无限缩放,而不会丢失保真度或增加额外字节,也不需要srcset!有一些方法可以通过 CSS 改进这种简单的解决方案,我们将在下一章中看到,但现在,只需知道这种模式将起作用。从一个 3000 多像素的巨型显示器到一个小巧的功能手机(假设它支持 SVG),前面的标记将很好地进行缩放。

艺术指导用例怎么样?使用 SVG 也更简单。因为我们不必提供图像的多个版本(每个 SVG 图像都可以根据需要进行缩放),艺术指导用例的标记如下。

我们有与之前看到的相同的picture元素。有一个子source元素,其中包含一个指向大于1024像素的浏览器的媒体查询。如果为true,则将使用横向图像。然后,有一个子img元素,其中srcset指向一个正方形图像,width为 100%。如果第一个source元素上的媒体查询失败,我们就会得到这个图像。

它不像普通的img那样简单,但比每个srcset中的多个位图图像版本要简单得多。输出两个图像,即可处理最复杂的情况,艺术指导和跨多个屏幕分辨率的缩放:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG- picture element with SVG </title>
  <script src="img/picturefill.min.js"></script>
</head>
<body>
  <picture>
    <source 
      media="(min-width: 1024px)"
      srcset="more-colors.svg">
    <img 
      src="img/more-colors-square.svg" 
      srcset="more-colors-square.svg"
      width="100%">
  </picture>
  </body>
</html>

虽然 SVG 并非适用于每种用例,但它是远远最灵活的 RWD 图像格式。从基于一个图像源的简单width="100%"技术来缩放图像,到使用picture元素更简单地实现艺术指导用例,SVG 在这个多分辨率、多设备的世界中提供了巨大的好处。

HTML 文档中内联 SVG 的附加细节

正如您已经了解的那样,使用内联 SVG 与 HTML 标记一样简单,通常是您嵌入 SVG 到文档中的最佳(或唯一,在交互式 SVG 的情况下)选项。也就是说,与网络上的任何内容一样,当使用内联 SVG 时,总会有一些边缘情况、注意事项和需要牢记的要点。本节概述了其中两个问题。第一个是尝试利用浏览器的缓存,另一个是要注意在处理 SVG 时 DOM 复杂性可能会大幅增加。

缓存

与作为img元素的src链接的 SVG 图像或通过 CSS 引用的 SVG 图像不同,内联 SVG 无法被缓存并在另一页或单页应用程序的不同视图中引用。虽然最小化 HTTP 请求的数量仍然有性能优势(内联 SVG 通过消除对单独 SVG 文档的请求来实现),但这并不总是最优化的模式。如果您在多个页面上多次使用相同的 SVG 图像,或者多次访问多个站点,那么拥有一个可以被缓存并稍后再次读取的文件将会有益处。这对于更大、更复杂的 SVG 图像尤其如此,它们可能具有较大的下载占用空间。

如果您真的需要使用内联 SVG(例如所有交互式示例),您仍然可以尝试通过使用use元素链接到外部库 SVG 元素来以不同的方式利用浏览器缓存。您可能会在前期添加一些 HTTP 请求,但您不必不断下载和解析定义这些可重用组件的内联标记。

而且,考虑可重用组件是思考项目结构的一种好方法,因此这是利用浏览器缓存之外的好处。

复杂性

虽然我实际上已经尝试限制您在本书中迄今为止看到的 SVG 代码示例的复杂性,但您已经看到了一些非常繁忙的 SVG 示例。实际上,当处理比一小部分rectcircletext元素更复杂的任何内容时,SVG 代码的大小和/或可读性可能会迅速下降。这在生成的代码中尤其如此,因为它实际上并不是为了人类消费而设计的。

这种复杂性可能会以两种不同的方式成为问题:创作环境更加复杂,页面的渲染和性能变慢。

创作

SVG 文档可能会变得非常庞大,即使是简单的图像也是如此。根据效果的数量和元素的数量,用于绘制 SVG 图像的标记可能会迅速压倒页面上的其他所有内容。因此,值得将大型 SVG 元素保留为单独的文档片段,并根据需要将它们引入您的文档中。根据它们的使用方式,这可能是在包含 SVG 文档内部使用use元素,或者可能是使用您选择的页面组合工具导入文档片段的情况。有许多服务器端和/或客户端解决方案可以将标记和文本片段组合在一起(例如,JavaScript 模板解决方案,CMS,博客平台和服务器端脚本语言,如 PHP),因此我不打算创建一个潜在有限用途的示例。我相信您会利用最贴近您心的解决方案。

您仍然必须在检查页面时处理它,但这比在一个 700 行文件中占据 500 行标记的 SVG 插图要好得多,该插图显示了供应链图表或类似内容。

文档对象模型

除了创作问题,您还可能遇到非常复杂的 SVG 在浏览器性能方面的问题。这是真实的,无论您以何种方式导入它们,因为即使 SVG 作为img src导入,它也不仅仅是一组像素,但如果您已经在 DOM 中进行了大量交互,这种情况可能会更加严重。一般来说,文档中的元素数量直接影响页面的速度和响应性(https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing)。当页面上有成百上千个潜在的可交互 SVG 元素时,每个元素都需要由浏览器计算(其中一些在内部进行非常复杂的计算)并呈现,事情可能会非常快地变慢。

大多数情况下,您不太可能遇到这种性能问题。至少我希望您不会。然而,这是可能的,因此请将可能性存档,希望您永远不必使用这些知识。

总结

在本章中,您了解了在 HTML 文档环境中使用 SVG。首先,您了解了使用内联 SVG 元素和 SVG 图像作为img元素的src时的 SVG 可访问性。这包括img元素的alt属性的详细信息,以及内联 SVG 中titledesc元素的详细信息。

接下来,您了解了响应式图像的解决方案,以及如何使用 SVG 可以极大地简化甚至是最复杂的响应式图像使用情况的实现。最后,您了解了在实际世界中实施这些解决方案时需要注意的内联 SVG 的其他方面。

接下来,我们将看一下 CSS 和 SVG 的重要交集。下一章将建立在我们所学到的一切基础上,并将为您介绍一些强大的新工具,供您添加到 SVG 工具箱中。

第五章:使用 SVG 和 CSS

本章将重点介绍 SVG 和 CSS 的交集。虽然 JavaScript 是处理 SVG 最强大的工具,但没有 CSS 的 SVG 不会像现在这样受欢迎。正如你已经了解的那样,SVG 非常适合现代网络,通常是对 RWD 问题的最佳答案。因此,它已经被设计师和开发人员全力以赴地用于为网络生成图像。

对于整个网络来说,这种对 SVG 的偏好是一个很好的选择,应该加以培养。本章将希望说明为什么。

在这一章中,我们将学习以下内容:

  • 使用 CSS 背景图像

  • 如何优化 SVG 的数据 URI

  • SVG 精灵与图标字体

  • 不同的 SVG 嵌入方式如何与 CSS 互动

  • 使用常见的 CSS 属性来操作 SVG

  • 使用 SVG 特定的 CSS 属性来操作 SVG

  • 使用 SVG 的基本 CSS 动画和过渡

CSS 背景图像

你已经在第一章中看到了使用 CSS 作为背景图像的例子,介绍可伸缩矢量图形。本节将为使用 SVG 的这种方式添加一些更多的细节。

在这个最初的基本示例中,我们将一个风格化的字母 R 的 SVG 图像添加为div的背景图像。一个重要的方面是设置background-size属性。SVG 图像的自然大小是458乘以392。在这种情况下,它被设置为原来大小的一半,以适应div的大小:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Background Images</title>
        <style type="text/css">
          .logo{
            width: 229px;
            height: 196px;
            background: url(icon.svg);
            background-repeat: no-repeat;
            background-size: 229px 196px;
          }
        </style>
    </head>
    <body>
        <div class="logo">
        </div>
    </body>
</html>

在浏览器中呈现,我们得到以下结果:

除了提供高像素密度显示(这确实是一个很棒的功能)之外,这并没有比 PNG 提供更多。

在使用相对单位的环境中,你可以利用 SVG 的能力,将background-size的值设置为containcover,以真正充分利用 SVG。在下面的例子中,与之前相同的标志被应用为背景图像,旁边还有一些文本。所有的度量都是相对的,使用根 em(rem)单位。背景图像设置为containbackground-size值。contain确保标志将完整地显示,受包含元素的高度和宽度的限制。由于我们使用 SVG 图像作为背景图像,文档的基本字体(因此计算根 em)可以从 16 像素(浏览器默认值)缩放到 1600 像素,SVG 背景将能够缩放以匹配:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- Relative Background Images </title>
        <link href="https://fonts.googleapis.com/css?
         family=Raleway:600" rel="stylesheet"> 
        <style type="text/css">
          .logo{
            width: 14.3rem;
            height: 14.3rem;
            background: url(icon.svg);
            background-repeat: no-repeat;
            background-size: contain;
            background-position-y: 2.5rem;
          }
          h1 {
            font-family: Raleway, sans-serif;
            font-size: 2rem;
          }
        </style>
    </head>
    <body>
      <div class="logo">

      <h1>Rob Larsen</h1>
    </div>
    </body>
</html>

在浏览器中呈现,我们得到以下结果:

这里没有什么新的东西,但这是现代网页上 SVG 的一个非常重要的用途,值得花一些时间来强调这种模式。

SVG 背景图像的数据 URL

如果你注重性能,你可能会想知道通过 data: URL 直接在 CSS 中嵌入背景图像的技术。数据 URL 允许你通过特殊的data: URL将文件直接嵌入文档中。这种技术允许你节省一个 HTTP 请求。

当使用诸如 JPG 或 PNG 之类的二进制格式时,图像数据需要进行base64编码。虽然这对 SVG 图像也适用,但实际上,将 SVG 图像作为 SVG 源嵌入更快(css-tricks.com/probably-dont-base64-svg/)。这是因为除了base64编码的数据外,你还可以直接嵌入文本。SVG 当然是一种文本格式。你只需要对 SVG 进行一些处理才能使其正常工作。你应该阅读 Taylor Hunt 的完整文章以获取详细信息(codepen.io/tigt/post/optimizing-svgs-in-data-uris),但基本步骤如下:

  • 使用单引号作为属性值

  • 对任何非安全字符(<>#等)进行 URL 编码

  • 双引号数据 URL

转换初始示例,我们得到的代码如下:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Background Images with Data 
         URLs</title>
        <style type="text/css">
          .logo{
            width: 229px;
            height: 196px;
            background: url("data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg' height='392' width='458'%3E%3Cg  stroke='%23000' stroke-width='14.17'%3E%3Cpath d='M96.42 60.2s14 141.5-58 289l145.5-18.4 55.4-276.7z' fill='%23000012'/%3E%3Cpath d='M145.42 188l108.5 171.6 189.2 24.4-123.4-196z' fill='%23000012'/%3E%3Cpath d='M70.12 43.7s14 141.5-58 289l145.5-18.4 55.4-276.7z' fill='%23e9c21b'/%3E%3Cpath d='M59.02 23.6l116.2 237.2c-.1 0 411.3-239.1-116.2-237.2z' fill='%23000012'/%3E%3Cpath d='M119.12 171.6l108.5 171.6 189.2 24.4-123.4-196z' fill='%233fc4eb'/%3E%3Cpath d='M32.62 7.1l116.2 237.2S560.22 5.2 32.62 7.1z' fill='%2359ea39'/%3E%3C/g%3E%3C/svg%3E");
            background-repeat: no-repeat;
            background-size: 229px 196px;
          }
        </style>
    </head>
    <body>
      <div class="logo">
    </div>
    </body>
</html>

虽然这实际上是非常简单的手工准备(这里的示例是手工编码的),但如果您想要挤出所有字节,有一些可用的工具可以为您完成这项工作。有一个 node 模块(www.npmjs.com/package/mini-svg-data-uri)和一个 SASS 函数(codepen.io/jakob-e/),可以帮助您将此功能构建到您的工作流程中。

SVG 精灵和图标集

这一部分并不严格涉及 CSS,但讨论了一种常见的基于 CSS 的解决方案的替代方案,用于向应用程序添加图标,因此这似乎是讨论它的最佳地方。

如果您正在阅读本书,您可能对图标字体的概念有所了解,比如 GLYPHICONS(glyphicons.com/)或 Font Awesome(fontawesome.com/icons?from=io)。如果您不了解,它们是字体,而不是表示可以作为语言阅读的字符(就像您现在正在阅读的字符),它们呈现可以用作站点或应用程序图标的不同图像。

例如,您可以使用Font Awesome创建视频播放器的界面,而无需设计单个元素。

以下代码示例显示了该实现可能看起来如何。除了 Font Awesome,以下示例还使用了 Bootstrap 样式。

Font Awesome 的基本模式是将图标作为空元素包含。在这种情况下是i。每个图标都有两个常见的类:fafa-2x。这些类表示元素是 Font Awesome 图标,并且应该以正常大小的2x渲染。之后,使用fa-类添加各个图标,表示要使用的图标类型:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- Font Awesome</title>
        <link rel="stylesheet" 
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <link href="font-awesome.min.css" rel="stylesheet" />
    </head>
    <body>       
        <div style="text-align: center">
            <button class="btn btn-link"><i class="fa fa-2x fa-backward
             "></i></button> 
            <button class="btn btn-link"><i class="fa fa-2x fa-fast-
              backward"></i></button> 
            <button class="btn btn-link"><i class="fa fa-2x fa-play">
             </i></button>
            <button class="btn btn-link"><i class="fa fa-2x fa-fast-
             forward"></i></button> 
            <button class="btn btn-link"><i class="fa fa-2x fa-
             forward"></i></button> 
        </div>

    </body>
</html>

在浏览器中呈现如下:

这一切都非常清晰易懂。正因为如此,这些图标字体非常受欢迎。我在多个环境中使用过它们,对它们的通用易用性和快速启动能力印象深刻。

也就是说,使用图标字体也有缺点。两个突出的缺点是:

  • 可访问性:有方法可以很好地使用图标字体,以便实现可访问性(www.filamentgroup.com/lab/bulletproof_icon_fonts.html),但开箱即用,您正在向空元素插入无意义的字符。屏幕阅读器可以读取这些无意义的字符,为依赖 AT 浏览网页的用户创建混乱的体验。

  • 语义:空元素是空的。使用ispan的图标字体实际上没有任何含义。

还有其他问题,包括加载 Web 字体的挑剔性以及对阅读障碍用户的问题(cloudfour.com/thinks/seriously-dont-use-icon-fonts/)。

好消息是,如果您对更好的语义、更好的可访问性和更直接的实现感兴趣,有一种 SVG 替代图标字体的方法:使用SVG 精灵。公平地说,SVG 精灵也不是一个完美的解决方案,因为最优雅的变体需要 IE/Edge 形状的解决方法。但对于某些配置(特别是单页应用程序),SVG 精灵是图标交付的绝佳选择。

让我们看看它是如何工作的。

在本例中,我们将使用 Front Awesome v5,它提供了所有图标的 SVG 版本,以复制先前的控件集。

以下是使用 SVG 精灵实现相同控件的方法。

首先,让我们看一下精灵文件本身的细节。在其中,所有图标都被定义为与 CSS 图标的类名相对应的symbol元素。每个symbol元素都包括可访问的title元素。

集合中的每个图标都在文件fa-solid.svg中表示:

 <symbol id="play" viewBox="0 0 448 512">
    <title id="play-title">play</title>
    <path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 
     40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"></path>
  </symbol> 

在 HTML 文件中,情况略有不同,但总体模式基本相同。我们仍然链接到 Bootstrap 以方便使用。我们不再在head中链接来自 Font Awesome 的任何内容。我们只需要一小块 CSS 来调整页面上图标的大小。在实际的示例中,您可能会做更多的样式处理,但目前这已经足够使其正常工作。

在文档的主体部分,我们有一个新的模式。与button.btn > i.fa模式不同,我们有button.btn > svg > use,其中 use 指向fa-solid.svg文件中的特定符号。

除此之外,我们有一个适用于 Internet Explorer 的问题。Internet Explorer 不允许您从外部文档中使用元素。脚本svg4everybody填补了这个缺陷,并允许您在 IE 中链接到外部 SVG:

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG- Font Awesome</title>
  <link rel="stylesheet" 
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0- 
    beta.3/css/bootstrap.min.css" integrity="sha384-
    Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy"
    crossorigin="anonymous">
  <style>
    .btn svg{
      height: 2em;
      width: 2em;
      fill: #007bff;
    }
  </style>
</head>
<body>
  <div>
    <button aria-label="rewind" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="fa-solid.svg#backward"></use>
      </svg>
    </button>
    <button aria-label="skip to previous track" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="fa-solid.svg#fast-backward"></use>
      </svg>
    </button>
    <button aria-label="play" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="fa-solid.svg#play"></use>
      </svg>
    </button>
    <button aria-label="skip to next track" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="fa-solid.svg#fast-forward"></use>
      </svg>
    </button>
    <button aria-label="fast forward" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="fa-solid.svg#forward"></use>
      </svg>
    </button>
  </div>
  <script src="img/svg4everybody.min.js"></script>
  <script>svg4everybody();</script>
</body>

</html>

我提到了单页面应用程序可能会被不同对待。如果您正在开发单页面应用程序并且想要使用 SVG 图标,您可以在页面中内联符号,并且在所有现代浏览器中使用它们,而无需任何填充脚本。对于单页面应用程序,您可能已经在文档中内联了 CSS 等内容以节省 HTTP 请求,因此在文档中内联一部分 SVG 可以成为相同过程的一部分。

我不打算详细说明这可能是如何从构建或页面创建的角度工作的,因为有很多种方法可以做到这一点(可以作为构建过程的一部分或通过服务器端模板系统),但输出可能看起来像以下代码示例。

最大的区别是在body顶部的内联svg元素中定义符号。这增加了页面的复杂性,但节省了 HTTP 请求。因此,如果您正在构建单页面应用程序并且不需要依赖缓存单独的精灵文件,这将会稍微更快一些。

除此之外,引用直接指向同一页面的文档片段,而不是链接到单独的文件。这意味着我们不需要 svg4everybody,而且 Internet Explorer 很乐意支持use

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- Font Awesome</title>
  <link rel="stylesheet" 
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-
    beta.3/css/bootstrap.min.css" integrity="sha384-
    Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy"
    crossorigin="anonymous">
  <style>
    .btn svg {
      height: 2em;
      width: 2em;
      fill: #007bff;
    }
  </style>
</head>

<body>
  <svg  style="display:none">
    <defs>
      <symbol id="play" viewBox="0 0 448 512">
        <title id="play-title">play</title>
        <path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 
         37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"></path>
      </symbol>
      <symbol id="fast-backward" viewBox="0 0 512 512">
        <title id="fast-backward-title">fast-backward</title>
        <path d="M0 436V76c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12
         12v151.9L235.5 71.4C256.1 54.3 288 68.6 288 96v131.9L459.5 
         71.4C480.1 54.3 512 68.6 512 96v320c0 27.4-31.9 41.7-52.5
         24.6L288 285.3V416c0 27.4-31.9 41.7-52.5 24.6L64 285.3V436c0 
         6.6-5.4
         12-12 12H12c-6.6 0-12-5.4-12-12z"></path>
      </symbol>
      <symbol id="fast-forward" viewBox="0 0 512 512">
        <title id="fast-forward-title">fast-forward</title>
        <path d="M512 76v360c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-
         12V284.1L276.5 440.6c-20.6 17.2-52.5 2.8-52.5-24.6V284.1L52.5 
         440.6C31.9 457.8 0 443.4 0 416V96c0-27.4 31.9-41.7 52.5-
         24.6L224 226.8V96c0-27.4 31.9-41.7 52.5-24.6L448 226.8V76c0-
         6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12z"></path>
      </symbol>
      <symbol id="forward" viewBox="0 0 512 512">
        <title id="forward-title">forward</title>
        <path d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 
         27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-
         256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 
         24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"></path>
      </symbol>
      <symbol id="backward" viewBox="0 0 512 512">
        <title id="backward-title">backward</title>
        <path d="M11.5 280.6l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-
         27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 
         49.2zm256 0l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-
         31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2z"></path>
      </symbol>
    </defs>
  </svg>
  <div>
    <button aria-label="rewind" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="#backward"></use>
      </svg>
    </button>
    <button aria-label="skip to previous track" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="#fast-backward"></use>
      </svg>
    </button>
    <button aria-label="play" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="#play"></use>
      </svg>
    </button>
    <button aria-label="skip to next track" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="#fast-forward"></use>
      </svg>
    </button>
    <button aria-label="fast forward" class="btn btn-link">
      <svg  role="img">
        <use xlink:href="#forward"></use>
      </svg>
    </button>
  </div>
</body>

</html>

与图标字体一样,SVG 精灵可以完全使用 CSS 进行自定义。您已经看到了一个示例,在前面的示例中我们改变了图标的大小和颜色。当您阅读本章的其余部分时,您将会遇到许多使用 CSS 操纵 SVG 的方法。这是一个非常强大的组合!

对内联 SVG 进行样式设置

本节将重点介绍您可以使用 CSS 操纵内联 SVG 元素的许多方法。本节不会详尽无遗,但将涵盖您在处理 SVG 时将使用的许多常见属性。

这些属性分为两类:

  • 您可能已经熟悉的 CSS 和 HTML 中的 CSS 属性,也适用于 SVG

  • 特定于 SVG 本身的 CSS 属性

让我们从熟悉的 CSS 属性开始。

使用常见的 CSS 属性来操纵 SVG

本节将重点介绍与 SVG 一起使用的常见 CSS 属性。除了一些例外,您实际上会关注的大多数属性都与文本相关。

基本字体属性

如果您已经使用 CSS 工作了一段时间,您可能已经操纵了元素的字体和样式。这些属性对 SVG 元素也是可用的。

以下代码示例显示了四个text元素。第一个没有应用任何样式,并显示了 SVG 中text元素的默认渲染。接下来的三个元素通过 CSS 样式进行了增强。第一个类text添加了优秀的 Raleway 字体(作为 Google 网络字体可用)和一个新的font-size2em)。接下来的两个类,text-italictext-bold,分别使用font-stylefont-weight进行了增强:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Font Properties</title>
        <link href="https://fonts.googleapis.com/css?
         family=Raleway:400" rel="stylesheet"> 

        <style type="text/css">
          .text {
            font-family: Raleway, sans-serif;
            font-size: 2em;
          }
          .text-italic {
            font-style: italic;
          }
          .text-bold {
            font-weight: bold;
          }
        </style>
    </head>
    <body>
      <svg  role="img" width="800"
        height="250" viewBox="0 0 800 250">
        <text x="25" y="50">
          Default text format
        </text>
        <text x="25" y="100" class="text">
          font-family: Raleway, sans-serif;
          font-size: 2em;
        </text>
        <text x="25" y="150" class="text text-italic">
          font-style: italic;
        </text>
        <text x="25" y="200" class="text text-bold">
          font-weight: bold;
        </text>
       </svg>
    </body>
</html>

在浏览器中呈现,您可以看到以下结果:

如果你在想,简写属性也同样适用。因此,简单地定义一个font属性是支持的,如下面的代码示例所示:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Font Shorthand</title>
        <link href="https://fonts.googleapis.com/css?
         family=Raleway:400" rel="stylesheet"> 
        <style type="text/css">
          .text {
            font: 2em bold Raleway, sans-serif; 
          }
        </style>
    </head>
    <body>
      <svg  role="img" width="800"
       height="250" viewBox="0 0 800 250">
        <text x="25" y="50">
          Default text format
        </text>
        <text x="25" y="100" class="text">
          font: 2em bold Raleway, sans-serif; 
        </text>
       </svg>
    </body>
</html>

这在浏览器中呈现如下:

文本属性

在 SVG 中支持的下一组 CSS 属性都与文本块有关。因此,不仅仅是由字体属性定义的单个字形,还有更大的字形组合方式。

以下代码示例展示了其中的几个。第一个类text再次更改了font-familyfont-size

接下来,我们有几个其他类,展示了 SVG 对文本属性的支持。第一个示例展示了对direction的支持,它允许您定义在从右到左阅读的语言中正常工作的文本块(例如,波斯语、阿拉伯语和希伯来语)。这个例子简单地将基于英语的属性定义锚定到框的右侧。接下来,我们将letter-spacing(跟踪)属性设置为宽敞的1em,使用text-decoration添加下划线,并将word-spacing设置为2em

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Text Properties</title>
        <link href="https://fonts.googleapis.com/css?
         family=Raleway:400" rel="stylesheet"> 

        <style type="text/css">
          .text {
            font-family: Raleway, sans-serif;
            font-size: 1.5em;
          }
          .text-direction {
            direction: rtl;
          }
          .text-letter-spacing {
            letter-spacing: 1em;
          }
          .text-decoration {
            text-decoration: underline;
          }
          .text-word-spacing {
            word-spacing: 2em;
          }
        </style>
    </head>
    <body>
      <svg  role="img" width="500"
       height="300" viewBox="0 0 500 300">
        <text x="475" y="50" class="text text-direction">
          direction: rtl;
        </text>
        <text x="25" y="100" class="text text-letter-spacing">
          letter-spacing: 1em;
        </text>
        <text x="25" y="150" class="text text-decoration">
          text-decoration: underline;
        </text>
        <text x="25" y="200" class="text text-word-spacing">
          word-spacing: 2em;
        </text>

       </svg>
    </body>
</html>

在浏览器中呈现,该示例如下所示:

杂项 CSS 属性

本节中的最后一个示例显示了对光标、显示和可见性属性的支持。其中,最有用的将是光标。在这个例子中,我们将rect元素的cursor更改为帮助光标。拖动手柄、调整大小手柄、可点击的指针等都将是交互式 SVG 中常用的值。

接下来,我们使用displayvisibility属性来隐藏元素。虽然在 HTML 中两者之间的区别很明显,在 SVG 中这两个属性之间的实际区别较小。在 HTML 中,使用display:none的元素不会影响文档的呈现。它们不会影响文档的整体流程。它们在 DOM 中,并且可以从 JavaScript 中访问,但实际上被呈现引擎忽略。另一方面,使用visibility:hidden设置的元素仍然是文档流的一部分。一个高 200 像素的div仍然会占据 200 像素。它只是以不可见的方式这样做。

由于 SVG 中的大多数元素都是在坐标系上使用(x,y)属性进行定位,因此两者之间的差异可能是微妙的。通常,使用visibility:hidden的 SVG 元素没有任何流程中断(tspan可能是一个例外),因此在布局上没有实际的区别。唯一的区别在于 JavaScript 事件的处理方式。我们将在后面更深入地研究这一点,既在下一节中,也在后面的 JavaScript 章节中。但根据pointer-events属性的设置方式,visibility:hidden元素可能仍然通过 JavaScript 事件与用户交互。默认情况下,它们不会,但这仍然是可能的:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Misc Properties</title>
        <link href="https://fonts.googleapis.com/css?
         family=Raleway:400" rel="stylesheet"> 

        <style type="text/css">
          .help {
            cursor: help;
          }
          .display-none {
            display: none;
          }
          .visibility-hidden {
            visibility: hidden;
          }
        </style>
    </head>
    <body>
      <svg  role="img" width="500" 
        height="300" viewBox="0 0 500 300">
        <rect x="10" y="0" width="100" height="100" fill="red" 
          class="help"></rect>
        <rect x="120" y="120" height="100" width="100" fill="blue" 
          class="display-none"></rect>
        <rect x="240" y="120" height="100" width="100" fill="blue" 
           class="visibility-hidden"></rect>
       </svg>
    </body>
</html>

在浏览器中呈现时,当鼠标悬停在元素上时,此示例如下所示:

如果您熟悉 CSS,那么您会知道display还有其他可能的值。虽然我相信设置 SVG 元素具有另一个display值的情况是有效的,但这不是您通常会做的事情,所以我不会在这里讨论这个问题。

使用 SVG 特定的 CSS 属性来操作 SVG

本节将讨论您可以用来处理 SVG 的不同 CSS 属性。这些属性中的大多数在以前的章节中已经作为特定 SVG 元素的属性看到了。您会发现这些表现属性的组合以及使用 CSS 在 SVG 元素和 SVG 文档之间共享样式的可能性代表了一个强大的组合。

CSS 属性将覆盖演示属性,但不会覆盖 style 属性(这实际上意味着 SVG + CSS 的行为方式与您熟悉的 CSS 特异性工作方式相同)。

颜色和绘画属性

这个第一个示例说明了更改元素填充的能力。fill 属性接受任何有效的 CSS 颜色值(developer.mozilla.org/en-US/docs/Web/CSS/color_value),以及指向绘图服务器的链接(例如,在 defs 部分中定义的 pattern)。fill-opacity 更改填充本身的不透明度(就像 rgba 颜色定义中的 alpha 值一样),而不是整个元素,就像 CSS 的 opacity 属性一样。

在这个例子中,我们定义了四个类。前两个,red-fillblue-fill,定义了两种不同的主要颜色,红色和蓝色,用于填充。第三个,half-opacity,定义了 50% 的不透明度。最后一个 gradient,定义了填充为 SVG 元素中定义的绘图服务器的链接。

然后,它们与您使用常规 HTML 元素时使用的相同的 class 属性一起应用:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Fill Properties</title>
        <link href="https://fonts.googleapis.com/css?
          family=Raleway:400" rel="stylesheet"> 

        <style type="text/css">
          .red-fill {
            fill: red;
          }
          .blue-fill {
            fill: blue;
          }
          .half-opacity{
            fill-opacity: .5;
          }
          .gradient{
            fill: url(#linear);
          }
        </style>
    </head>
    <body>
      <svg  role="img" width="550" 
        height="300" viewBox="0 0 550 300">
          <defs>
            <linearGradient id="linear">
                <stop offset="5%" stop-color="green"/>
                <stop offset="95%" stop-color="gold"/>
            </linearGradient>

        </defs>
        <rect x="10" y="0" width="100" height="100" class="red-fill">
        </rect>
        <rect x="120" y="0" height="100" width="100" class="blue-fill">
        </rect>
        <rect x="230" y="0" height="100" width="100" class="blue-fill 
         half-opacity" ></rect>
        <rect x="340" y="0" height="100" width="100" class="gradient">
        </rect>
       </svg>
    </body>
</html>

在浏览器中呈现,我们得到以下结果:

描边属性

另一个非常有用的一组属性,可用于从 CSS 操作 SVG 的属性与描边有关。所有描边属性都可以作为 CSS 属性使用。与 fill 属性类似,这些属性在创建一致的界面和可视化方面非常有用。

这个例子展示了作为基本 stroke 类的一部分使用 strokestroke-width 的用法。这样设置了一个常见的描边样式,以便我们可以将其他描边操作属性应用到我们的示例中。在那之后,我们设置了两个虚线属性,stroke-dashoffsetstroke-dasharray,并将这些属性应用到前两个 rect 元素,使用 stroke-dasharraystroke-dashoffset 类。之后,我们使用 stroke-linecap-join 类将 stroke-linecap 应用到 line 元素。在那之后,我们将 stroke-linejoin-round 类应用到最后一个 rect 元素。

property/value 对匹配了您在 第二章 中学到的相同模式,开始使用 SVG 进行创作,当您最初学习这些演示属性时。

所有这些都可以作为 CSS 属性使用,这应该有助于您为 SVG 文档中的元素创建一致可重用的描边模式:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Mastering SVG- CSS Stroke Properties</title>
        <link href="https://fonts.googleapis.com/css?
         family=Raleway:400" rel="stylesheet"> 

        <style type="text/css">
            .stroke {
                stroke-width: 10px;
                stroke: royalblue;
            }
            .stroke-dasharray {
                stroke-dasharray: 10;
            }
            .stroke-dashoffset {
                stroke-dashoffset: 25;
            }
            .stroke-linecap-square {
                stroke-linecap: square;
            }
            .stroke-linejoin-round{
                stroke-linejoin: round;
            }
            .stroke-opacity{
                stroke-opacity: .5;
            }
        </style>
    </head>
    <body>
      <svg  width="550" height="300" 
       viewBox="0 0 550 300">
        <rect x="50" y="15" width="300" height="50" fill="none"
        class="stroke stroke-dasharray"></rect>
        <rect x="50" y="80" width="300" height="50" fill="none"
        class="stroke stroke-dasharray stroke-dashoffset"></rect>
        <line x1="50" y1="160" x2="350" y2="160" class="stroke stroke-
          linecap-square"></line>
        <rect x="50" y="180" width="300" height="50" fill="none"
        class="stroke stroke-linejoin-round"></rect>
       </svg>
    </body>
</html>

在浏览器中呈现,上述代码产生以下输出:

文本属性

本节将介绍一些 SVG 特定的文本属性。前几个示例涉及 SVG 中文本的基线。根据您的工作类型,您可能永远不需要调整文本元素的基线(文本行所在的视觉平面)。但是,您可能需要,特别是如果您正在处理多语言布局或复杂的基于文本的插图(如标志)。因此,值得向您介绍这些属性。与基线相关的属性是 alignment-baselinedominant-baselinebaseline-shift

除此之外,本节还将介绍 text-anchor 属性,该属性可以更改 text 元素的锚点。

关于基线属性的简要说明,还有更多内容,但以下描述足以为您提供足够的基础,以理解代码示例中发生的情况。这 可能 足够让您使用这些属性:

  • dominant-baseline 用于调整 text 元素的基线

  • alignment-baseline 用于调整子元素相对于其父 text 元素基线的基线

  • baseline-shift可能是最有用的,通过将主基线上移或下移来提供常见的下标上标功能

dominant-baselinealignment-baseline接受类似的值。这里使用的两个值是hanging,它将文本从文本框底部删除,以及middle,它将文本垂直居中在文本框底部。在这个示例中,dominant-baseline应用于具有两个不同值的text元素,而alignment-baseline应用于具有两个不同值的两个子tspan元素。

在此之后,使用baseline-shiftsupersub值创建了常见的上标和下标模式。

最后,text-anchor属性通过应用于视口中间居中的文本元素的三个不同值进行说明。text-anchor将文本对齐到文本框中的不同点:startmiddle和句子的end

接下来的代码示例说明了这些基线属性的用法,以及text-anchor属性的用法:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- SVG-specific CSS Text Properties</title>
    <link href="https://fonts.googleapis.com/css?family=Raleway:400"
      rel="stylesheet">

    <style type="text/css">
     .text {
         font-family: Raleway, sans-serif;
         font-size: 1.5em;
     }
     .dominant-hanging {
         dominant-baseline: hanging;
     }
     .dominant-middle {
         dominant-baseline: middle;
     }
     .alignment-hanging {
         alignment-baseline: hanging;
     }
     .alignment-middle {
         alignment-baseline: middle;
     }
     .sub {
        baseline-shift: sub;
    }
    .super {
        baseline-shift: super;
    }
    .text-anchor-start{
        text-anchor:start;
    }
    .text-anchor-middle{
        text-anchor:middle;
    }
    .text-anchor-end{
        text-anchor:end;
    }
    </style>
</head>

<body>
    <svg  width="400" height="550"
      viewBox="0 0 400 550">
        <rect width="400" height="25" x="0" y="0" fill="#cccccc" />
        <rect width="400" height="25" x="0" y="25" fill="#efefef" />
        <rect width="400" height="25" x="0" y="50" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="75" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="100" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="125" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="150" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="175" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="200" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="225" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="250" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="275" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="300" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="325" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="350" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="375" fill="#efefef"/>
        <rect width="400" height="25" x="0" y="400" fill="#cccccc"/>
        <rect width="400" height="25" x="0" y="425" fill="#efefef"/>

        <line x1="200" y1="300" x2=
        "200" y2="325" stroke="red"></line>
        <line x1="200" y1="350" x2=
        "200" y2="375" stroke="red"></line>
        <line x1="200" y1="400" x2=
        "200" y2="425" stroke="red"></line>
        <text class="text dominant-hanging" x="50"
          y="25">Hanging</text>
        <text class="text dominant-middle" x="50" y="75">Middle</text>
        <text class="text" x="50" y="125">Text <tspan class="alignment-
         hanging">Hanging</tspan></text>
        <text class="text" x="50" y="175">Text <tspan class="alignment-
         middle">Middle</tspan></text>
        <text class="text" x="50" y="225">Super<tspan
         class="super">sup</tspan></text>
        <text class="text" x="50" y="275">Sub<tspan 
         class="sub">sub</tspan></text>
        <text class="text text-anchor-start" x="200" y="325">Text
          Anchor Start</text>

        <text class="text text-anchor-middle" x="200" y="375">Text
         Anchor Middle</text>

        <text class="text text-anchor-end" x="200" y="425">Text Anchor 
         End </text>

    </svg>
</body>

</html>

在浏览器中呈现,这些效果在以下截图中可见:

较暗的条带显示了基于文本元素的xy位置的初始文本框。您可以看到hangingmiddle如何清楚地移动了字体的基线,相对于xy位置。

text-anchor示例通过添加一条指示这些文本元素的(x,y)位置的线来进行说明。它们放置在 SVG 元素的中心,这说明了该属性对文本元素的影响。

合成属性

目前,浏览器对合成属性的支持非常糟糕。在撰写本文时,微软对clip属性的支持不完整,而 mask 属性的支持在各个浏览器中都很糟糕。这很不幸,因为它们可以提供强大的选项来定义和重用剪切路径和蒙版。

我要展示的一个有效示例是如何使用 CSS 定义clip-path。有两种变体。第一种只是通过id引用了一个clipPath元素。这很简单,在现代浏览器中可以工作。

第二个示例允许更好地分离关注点。您不必定义一个带有路径的元素来进行剪切,而是可以直接向 CSS 提供多边形坐标。polygoncircleinset是此属性的可用值。这种语法取代了现在已弃用的clip属性。如果您熟悉clip,您应该注意几件事。首先,请注意没有rect值的直接替代品。值得庆幸的是,正如我们在这里展示的,多边形已经足够替代rect。其次,clip-path不需要将元素绝对定位(尽管在 SVG 中使用此属性时,这并不是一个特别关注的问题)。

多边形值的语法与用于polygon元素的path属性的语法略有不同。与路径元素的 d 属性的逗号是任意的,仅用于可读性不同,这个 CSS 属性中的点对需要用逗号分隔并且需要单位。否则,它的工作方式与 SVG 中的polygon相同。

这个示例通过将点映射为polygon来复制clipPath示例中看到的矩形:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- CSS Compositing Properties</title>
    <link href="https://fonts.googleapis.com/css?family=Raleway:400" 
      rel="stylesheet">

    <style type="text/css">
      .clip-url{
        clip-path: url(#box);
      }
      .clip-polygon {
        clip-path: polygon(50% 0, 100% 0, 100% 100%, 50% 100%, 50% 0)
      }
    </style>
   </head>
   <body>
     <svg  width="240" height="240"
       viewBox="0 0 240 240" version="1.1">
       <defs>
         <clipPath id="box" maskUnits="userSpaceOnUse" x="0" y="0" 
           width="240" height="240">

             <rect x="120" y="0" width="240" height="240" fill="red" >
             </rect>
         </clipPath>
         <polygon id="star" points="95,95 120,5 150,95 235,95 165,150 
           195,235 120,180 50,235 75,150 5,95"></polygon>
       </defs> 
       <use href="#star" fill="red"></use>
       <use href="#star" fill="black" class="clip-url"></use>
     </svg>
     <svg  width="240" height="240"
        viewBox="0 0 240 240" version="1.1">
      <defs>
        <polygon id="star" points="95,95 120,5 150,95 235,95 165,150 
          195,235 120,180 50,235 75,150 5,95"></polygon>
      </defs> 
      <use href="#star" fill="red"></use>
      <use href="#star" fill="black" class="clip-polygon"></use>
    </svg>
   </body>
</html>

在浏览器中呈现,您将获得以下输出:

正如我所提到的,对mask属性的支持存在问题,因此我没有完全实现的示例。有三种定义的模式:

  • 第一个与clip-path属性类似。您可以定义一个mask-image属性,并通过url将一个蒙版图像传递给它:
.mask{
    mask-image: url(mask.svg);
}
  • 第二个选项是使用片段标识符来使用链接图像的一部分:
.mask-fragment{
    mask-image: url(mask.svg#fragment);
}
  • 第三个,也是最有趣的选项,允许您在属性值中创建蒙版:
.mask-image {
    mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent);
}

这项技术尚未准备好投入使用,但了解未来的技术发展是很重要的,特别是它将允许您仅使用 CSS 类来重用在一个中心位置定义的蒙版。

交互属性

我们要查看的最后一个 CSS 属性是pointer-events属性。pointer-events属性指示 SVG 元素是否可以成为指针事件的目标(包括所有输入,包括鼠标、笔或触摸输入)。

实现pointer-events的基本方法是打开或关闭它们。以下示例展示了这一点。此示例还将包括一点 JavaScript,这样您就可以在第六章 JavaScript 和 SVG中提前了解一些关于使用 JavaScript 操纵 SVG 的知识。

在此示例中,我们有两个rect元素。其中一个设置了类名pointer-default。该类有一个名为pointer-events的属性,值为visiblePaintedvisiblePainted是 SVG 元素上pointer-events的默认值。它表示元素的整个可见绘制区域应接受鼠标事件。这意味着边框和填充区域都包括在内。

第二个rect的类名是pointer-none。其单个属性pointer-events的值为none。这表示该元素不应接收鼠标事件。

页面底部有一个小的 JavaScript 块,展示了该属性的作用。它还说明了在处理 SVG 和 JavaScript 时可能遇到的差异。在其中,我们使用一些核心文档对象模型DOM)方法来为每个rect元素附加点击事件处理程序。首先,我们使用document.querySelectorAll来获取页面上所有rect元素的引用。如果您不熟悉它,querySelectorAll可以被视为著名的 jQuery 接口的标准化、浏览器原生版本。您传入一个 CSS 选择器,它返回一个包含查询结果的静态nodeList

我们立即通过方便的forEach方法循环遍历类似数组的nodeList,并为每个节点附加事件处理程序。此事件处理程序旨在在单击方块时更改相邻text元素的文本。

如果您习惯使用innerHTML来设置文本内容,您会注意到这里使用的是textContent属性。为什么呢?因为 SVG 没有innerHTML(这是有道理的,因为它不是 HTML)。

在浏览器中运行此代码,您会发现只有默认pointer-events值的rect单击才会更改文本。设置为nonerect不会有任何反应:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- CSS Compositing Properties</title>
    <link href="https://fonts.googleapis.com/css?family=Raleway:400" 
      rel="stylesheet">

    <style type="text/css">
      .pointer-default {
        pointer-events: visiblePainted;
      }
      .pointer-none {
        pointer-events: none;
      }

    </style>
   </head>
   <body>
     <svg  width="500" height="250" 
      viewBox="0 0 500 250" version="1.1">
       <rect x="10" y="10" width="100" height="100" class="pointer-
         default" fill="red"></rect>
       <rect x="120" y="10" width="100" height="100" class="pointer-
          none" fill= "red"></rect>
       <text x="10" y="150" id="text"></text>
    </svg>
    <script>
      document.querySelectorAll("rect").forEach(function(element){
        let classname = element.className.baseVal;
        element.addEventListener("click",()=>{
          document.getElementById("text").textContent= `clicked
           ${classname}`
        });
      });
    </script>
   </body>
</html>

以下插图显示了点击两个rect元素后的页面:

下表说明了此属性的其他可能值。它们在与 SVG 元素交互的方式上提供了很多控制。根据您计划进行多少精确的交互,您可能最终会利用这种精度:

属性 定义
visiblePainted 如果visibility属性设置为visible,并且指针位于painted area上,则可以将该元素作为目标。使用此值,绘制区域包括stroke(如果它设置为除none之外的值)和fill(如果它设置为除none之外的值)。
visibleFill 如果visibility属性设置为visible,并且指针位于内部(fill区域)上,则可以将该元素作为目标,无论fill是否设置。
visibleStroke 如果visibility属性设置为visible,并且指针位于周边(stroke区域)上,则可以将该元素作为目标,无论stroke是否设置。
visible 如果visibility属性设置为visible并且指针位于内部或周边,则可以定位元素,无论是否设置了 fill 或 stroke。
painted 如果visibility属性设置为visible并且指针位于painted area上,则可以定位元素。使用此值,绘制区域包括stroke(如果设置为除none之外的值)和fill(如果设置为除none之外的值)。不考虑visibility属性的值。
fill 如果指针位于内部(fill区域),则可以定位元素,无论是否设置了 fill。不考虑visibility属性的值。
stroke 如果指针位于周边(stroke区域),则可以定位元素,无论是否设置了 fill。不考虑visibility属性的值。
all 如果指针位于元素的内部或周边,则可以定位元素。不考虑strokefillvisibility属性的值。
none 元素不接收指针事件。

独立 SVG 图像中的样式

到目前为止,所有的例子都是关于内联 SVG 在 HTML 文档中,你也可以在独立的 SVG 图像中使用 CSS。以下 SVG 图像显示了使用 CSS 来调整多个 SVG text 元素的显示。有趣的细节包括包裹在style元素中的样式的字符数据<![CDATA[ ]]>)块:

如果你没有处理过很多 XML(它现在并不像以前那样常见,所以可能是这种情况),CDATA用于指示 XML 解析器,该部分可能包含可被解释为 XML 但不应该的字符。 JavaScript(由于<>的普遍存在)是最常见的用例(如果你在 1999 年构建网站,你会知道),但 CSS 也可能陷入同样的陷阱,所以在这里使用它也是很好的。

接下来要注意的是外部样式表的缺失。如果要创建一个将作为img src导入或作为 CSS 背景图像的 SVG 图像,它需要完全自包含。

除此之外,这与你可能熟悉的 HTML 和 CSS 组合工作方式非常相似:

<?xml version="1.0" encoding="UTF-8"?>
    <svg  width="250" height="250" viewBox="0 0 250 250" version="1.1">
    <style>
      <![CDATA[
          text {
            font-family: Verdana, Geneva, sans-serif;
            fill: slategray;
          }
          .palatino{
            font-family: Palatino, "Palatino Linotype", "Palatino LT
            STD", "Book Antiqua", Georgia, serif;
          }
          .big-green{
            fill: forestgreen;
            font-size: 2rem;
            opacity: .75;
          }
          .huge-blue{
            fill: dodgerblue;
            font-size: 4rem;
          }
          .medium-deep-pink{
            fill: deeppink;
            font-size: 1.5rem;
          }
          .bigger {
            font-size: 6rem;
          }
          .text-anchor-middle{
            text-anchor: middle;
          }
          .text-baseline-middle{
            dominant-baseline: middle;
          }
          .half-opacity{
            opacity: .5;
          }
        ]]>
       </style>
          <text x="20" y="20" class="big-green">Styles</text>
          <text x="-10" y="50" class="huge-blue palatino">Styles</text>
          <text x="66" y="40" class="medium-deep-pink half
           opacity">Styles</text>
          <text x="77" y="77" class="big-green">Styles</text>
          <text x="55" y="66">Styles</text>
          <text x="100" y="125" class="medium-deep-pink 
           bigger">Styles</text> 
          <text x="175" y="33" class="big-green">Styles</text>
          <text x="220" y="44" class="huge-blue half-
            opacity">Styles</text>
          <text x="-20" y="244" class="huge-blue bigger half-
            opacity">Styles</text>
          <text x="120" y="120" class="medium-deep-pink">Styles</text>
          <text x="14" y="166" class="big-green palatino">Styles</text>

          <text x="136" y="199" class="huge-blue palatino half-
            opacity">Styles</text>
          <text x="170" y="144" class="huge-blue">Styles</text>
          <text x="-40" y="144" class="huge-blue half-
            opacity">Styles</text>
          <text x="143" y="24" class="big-green">Styles</text> 
          <text x="125" y="125" class="bigger text-anchor-middle text- 
            baseline-middle">Styles</text> 
      </svg>

在浏览器中呈现,此图像如下所示:

使用 SVG 的基本 CSS 动画和过渡

使用 SVG 和 CSS 的最有趣的方式之一是使用 CSS 动画过渡

  • 动画:这允许你为元素分配动画。这些动画被定义为一系列对 CSS 属性的更改。

  • 过渡:这允许你控制 CSS 属性更改生效所需的时间。它们不是立即改变,而是在状态之间过渡

这些是一组非常强大的功能,并且是 SVG 工具包的重要概念和技术补充。

CSS 动画

SVG 中的 CSS 动画与 HTML 中的工作方式相同,还可以使用 SVG 特定的属性。

基本动画格式

基本模式如下。SVG 很简单。它是一个单独的rect元素。CSS 有两个有趣的组件。第一个是类rect,它引用了一个属性animationanimation是一个简写属性,映射到一整套animation-属性。在这种情况下,我们设置了其中的两个。映射属性的第一个是animation-name,它引用了@keyframes动画中定义的动画movement。我们设置的第二个是animation-duration,我们将其设置为三秒(3s)。@keyframes动画是魔术发生的地方。在其中,我们设置了两组关键帧。第一组标记了动画的初始(0%)和最终状态(100%),使用了相同的属性,即 CSS transform,设置为(0,0)translate函数。这是初始(和最终)状态。我们将在下一个关键帧中对transform属性进行动画。在其中,我们设置了动画的中间部分(50%),我们将rect向右移动 400 像素:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- CSS animation</title>

    <style type="text/css">
    .rect {
      animation: movement 3s;
    }

    @keyframes movement {
      0%, 100% {
        transform: translate(0, 0);
      }
      50% {
        transform: translate(400px, 0);
      }
    }

    </style>
   </head>
   <body>
     <svg  width="500" height="100" 
       viewBox="0 0 500 100" version="1.1">
      <rect x="0" y="0" width="100" height="100" class="rect">
    </svg>
   </body>
</html>

效果是矩形慢慢从左到右移动,然后再返回:

使用动画剪辑路径

从 CSS 的角度来看,SVG 中动画剪辑路径的一个相对简单的例子是非常强大的。使用我们刚学到的polygon选项,你可以在两个(或更多)被定义为clip-path的形状之间进行动画。如果它们有相同数量的点,浏览器将平滑地在动画中定义的位置之间进行动画。

以下示例就展示了这一点。在这个示例中,我们创建了一个类stars

  • stars有一个animation属性。它引用了样式表中稍后定义的@keyframe stars块。

  • 第二个参数你已经熟悉了,animation-duration。这次又设置为三秒。

  • 第三个属性对你来说可能是新的。属性值infinite映射到animation-iteration-count属性。

  • animation-iteration-count接受一个数字,表示动画应该运行的具体次数,或关键字infinite,表示动画应该永远播放。

@keyframes遵循与之前动画相同的模式。我们有相同的起始和完成状态(0%和 100%)。这些被定义为一个多边形clip-path,用于说明一个星星。动画的中点(50%)将多边形重新定义为正方形。由于动画状态之间需要等效的点数,这意味着我们需要定义多于四个点来在这些状态之间进行动画:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- CSS animation</title>

    <style type="text/css">
    .stars {
      animation: stars 3s infinite;
    }

    @keyframes stars {
      0%, 100% {
        clip-path: polygon(95px 95px, 120px 5px, 150px 95px, 235px 
         95px, 165px 150px, 195px 235px, 120px 180px, 50px 235px,75px
         150px, 5px 95px)
      }
      50% {
        clip-path: polygon(10px 10px, 120px 10px, 230px 10px, 230px 
        120px, 230px 180px, 230px 230px, 120px 230px, 10px 230px, 10px
        180px, 10px 120px)
      }
    }

    </style>
   </head>
   <body>
     <svg  width="240" height="240"
      viewBox="0 0 500 500" version="1.1">
      <image href="take-2-central-2017.jpg" width="1000" height="500" 
       x="0" y="0" class="stars"></image>
    </svg>
   </body>
</html>

以下时间间隔截图显示了动画在运行的三秒内是如何展开的。确保在支持的浏览器中运行,看看这种效果有多有趣:

同时对一个元素进行多个属性的动画和分配多个动画

关于动画还有两件事需要注意:

  • 你可以同时对多个 CSS 属性进行动画

  • 你也可以将多个动画应用于同一个元素

以下代码示例显示了这两个功能的工作原理。有三个重要部分。第一个是单个类rect。它有两个用于animation属性的逗号分隔的参数,即动画boxchange-color-and-fadebox定义了两个正方形clip-path属性,一个距离矩形边缘50像素,另一个距离边缘10像素。change-color-and-fade将背景颜色从红色变为蓝色,不透明度从.5变为1

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- CSS animation</title>
  <style type="text/css">
    svg {
      background: lightgray;
    }
    .rect {
      animation: box 3s infinite, change-colors-and-fade 3s infinite;
    }
    @keyframes box {
      0%,
      100% {
        clip-path: polygon(50px 50px, 200px 50px, 200px 200px, 50px 
        200px)
      }
      50% {
        clip-path: polygon(10px 10px, 240px 10px, 240px 240px, 10px 
        240px)
      }
    }
    @keyframes change-colors-and-fade {
      0%,
      100% {
        opacity: .5;
        fill: rgb(255, 0, 0);
      }
      50% {
        opacity: 1;
        fill: rgb(0, 0, 255);
      }
    }
  </style>
</head>

<body>
  <svg  width="250" height="250"
   viewBox="0 0 250 250" version="1.1">
    <rect x="0" y="50" width="250" height="50" fill="gray"></rect>
    <rect x="0" y="0" width="250" height="250" class="rect"></rect>
  </svg>
</body>

</html>

在浏览器中运行,动画经历以下阶段:

CSS 过渡

本章中我们要看的最后一个 CSS 属性是 CSS transition属性。transition允许您定义浏览器在属性值更改时的动画方式。属性不会立即改变,而是可以更平滑地过渡。

以下示例展示了这是如何工作的。在示例中,我们有一个小的、单值的条形图,当用户悬停在上面时会填充,显示对目标的虚拟进度。

CSS 中充满了用于定义文本的类。您会注意到本章中学到的许多属性。除了这些属性,您在本章中应该对它们有一定的了解之外,还有一些定义条形图的类,其中一个比另一个更有趣。

第一个the-bar定义了条形图的轮廓。第二个fill-the-bar定义了条形图的进度部分。它没有描边,填充为绿色。对我们来说有趣的部分是transition属性。transition是一组相关的transition-属性的简写。在这种情况下,我们使用了transition-propertytransform)和transition-duration3s)。这表示浏览器应该监视此元素上transform属性的更改,并在三秒内过渡到该属性的更改。在这个类中,我们还定义了一个scaleY transform,值为1,并使用transform-origintransform锚定到元素的bottom。我们需要一个基准的scaleY,这样浏览器就有一个匹配的属性来进行动画。fill-the-bar:hover将比例改变为7.5,根据配置的方式,这将填满条形图的75%目标:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG- CSS Transitions</title>
  <link href="https://fonts.googleapis.com/css?family=Raleway:400" 
   rel="stylesheet">

  <style type="text/css">
    .text {
      font-family: Raleway, sans-serif;
      font-size: 1.5em;
    }

    .smaller-text {
      font-size: 1em;
    }

    .the-bar {
      stroke: black;
      stroke-width: 2px;
      fill: none;
    }

    .fill-the-bar {
      transition: transform 2s;
      transform: scaleY(1);
      transform-origin: bottom;
      stroke: none;
      fill: green;
      cursor: pointer;
    }

    .fill-the-bar:hover {
      transform: scaleY(7.5);
    }

    .dominant-baseline-hanging {
      dominant-baseline: hanging;
    }

    .dominant-baseline-middle {
      dominant-baseline: middle;
    }

    .text-anchor-end {
      text-anchor: end;
    }
  </style>
</head>

<body>
  <svg  width="250" height="500" 
   viewBox="0 0 250 500" version="1.1">
    <text class="text" x="10" y="25">Our Progress</text>
    <text x="90" y="50" class="dominant-baseline-hanging smaller-text
     text-anchor-end">100%</text>
    <text class="text smaller-text text-anchor-end" x="90" y="250">0%
    </text>
    <text class="text smaller-text text-anchor-end dominant-baseline-
     middle" x="90" y="150">50%</text>

    <rect x="100" y="50" height="200" width="50" class="the-bar">
    </rect>
    <rect class="fill-the-bar" x="100" y="230" height="20" width="50" 
     fill="green"></rect>
  </svg>
</body>

</html>

在浏览器中运行;过渡效果会慢慢增长,直到填满适当的空间:

总结

在本章中,您学到了很多。CSS 是制作快速、易于维护的现代网站和应用的关键技术之一,了解 SVG 和 CSS 之间的交集是很重要的。

本章以详细介绍了将 SVG 用作 CSS 背景图像的常见用例,包括使用 SVG 数据 URL 的有趣细节。

接下来,您将学习 SVG 精灵和图标集,以及它们可以如何以及为什么可以用来替代当今网页上流行的常见字体图标集。

接下来,您将学习如何对内联 SVG 进行样式设置,包括详细的操作字体和文本行的方式。接着,您将学习许多控制fillstroke和元素文本的 SVG 特定属性。之后,您将了解一些尖端的合成属性,比如clip-pathmask-image,尽管浏览器的支持还不完全到位,但它们非常强大。

之后,您将学习如何使用 CSS 来提高独立 SVG 图像的一致性和编写的便利性。

最后,您将学习如何使用基本的 CSS 动画和过渡效果来增加网站和应用的交互性和动态效果。

接下来,我们将把我们学到的关于将 SVG 放到页面上并确保它看起来正确的所有知识,加上 JavaScript,这样我们就可以开始以越来越有趣的方式与 SVG 进行交互了。

第六章:JavaScript 和 SVG

到目前为止,您已经在本书中学到了很多关于 SVG 的知识。您花了很多时间研究 SVG 规范的细节,以及 SVG 与 CSS 和 HTML 交互的不同方式。希望对您来说和对我一样有趣。

尽管一切都很有趣,但正是在这一章中,我们将把所有工具整合在一起,真正释放 SVG 的力量。将 JavaScript 添加到其中会开启大量新的可能性。

可以肯定,有许多网页开发人员和设计师永远不会使用 SVG 进行动画、动态可视化或其他交互式工作。对于他们来说,全面了解 SVG 本身作为标记的工作方式以及它如何与其他静态网页技术结合是非常有价值的。这在我们刚刚详细了解的 SVG 和 CSS 的交集中尤为重要。

话虽如此,SVG 最令人兴奋的是它如何轻松地与 JavaScript 一起工作,以增强您网站的交互性。所有这些开放网络技术都旨在以创造超越个别规范总和的方式相互配合。尽管有专门的专家在所有这些不同的技术上工作,但他们大多数情况下是公开的,并且通常是作为微软、谷歌或 Mozilla 等更大组织的一部分,因此他们真正希望确保这些技术以最佳方式相互配合。

SVG 和 JavaScript 的交集绝对是这样一个案例。

在本章中,我们将学习 JavaScript 和 SVG 之间的低级接口。这包括 SVG 的 DOM 接口。这是重要的内容,尽管我们也将学习使用 SVG 的库和框架。即使您已经从事网页开发一段时间,并熟悉 JavaScript 和 DOM,了解常规 HTML DOM 和 SVG 元素接口之间的差异也是重要的。如果您对原始 DOM 操作不太熟悉(许多在 jQuery 时代及以后开始的开发人员并不熟悉),那么本章将为您提供一整套有用的技能。

在本章中,我们将学习以下内容:

  • SVG 基本 DOM 接口-在 JavaScript 中访问和操作 SVG 元素

  • SVG 特定的 DOM 接口

  • 动态处理 SVG 和 CSS

JavaScript 版本和工具

在我们开始编码之前,我认为重要的是了解不同的 JavaScript 版本以及它们在本书中的使用方式。我还想介绍一下我将如何呈现需要工具的示例。

JavaScript 版本

您可能已经注意到,在过去几年中,围绕 JavaScript 编程语言的发展进行了大量工作。其中一些工作确实非常出色。事实上,目前网络上主要的库和框架都是用不同版本和变体的 JavaScript 写成的,这些 JavaScript 并不是在所有网络浏览器中都通用。使用最新版本的语言,包括特定于框架的扩展,是可能的,因为使用了转译器(scotch.io/tutorials/javascript-transpilers-what-they-are-why-we-need-them),这是一种软件,它将用一种语言(或在本例中是语言的一个版本)编写的软件代码转换为另一种语言(在本例中是语言的一个较旧但完全支持的版本)。这种转译步骤使我们能够用我们喜欢的 JavaScript 风格编写应用程序,然后将其转换为可以在任何地方运行的浏览器标准 JavaScript。

本节概述了您将在本书中遇到的不同 JavaScript 版本。下一节将简要介绍我们将如何呈现所需的工具,以使用转译器使您的最新代码在常见的网络浏览器中运行。

需要注意的是,这是对这个主题的最广泛的介绍。随着情况的出现,书中将涵盖更多细节,但即使如此,也只是触及了这个广泛主题的表面。

虽然我在整本书中都称呼并将继续称呼这种语言为 JavaScript,但这个商标名称(由 Oracle 商标,后者从 Sun Microsystems 获得商标,后者又从 Netscape 获得商标)并不是这种语言的官方名称。这种语言的官方名称是 ECMAScript,基于 Ecma (www.ecma-international.org/),这个组织主持编写规范的标准机构。

ECMAScript 5

ECMAScript 5 (ES5) 是当今浏览器中最完全支持的语言版本,也是转译器的目标版本,因为它可以在任何地方运行。标准化于 2009 年,截至撰写本文时,这个版本在超过 90%的浏览器中得到了全面支持,在约 97%的浏览器中得到了部分支持。通过添加 ES5 polyfills (github.com/es-shims/es5-shim),你可以几乎实现对 ES5 的普遍覆盖。一些代码,特别是第七章中的 Angular 1 和 jQuery 部分,常见的 JavaScript 库和 SVG,将直接以 ES5 编写。这是因为大多数人对 Angular 1 和 jQuery 都是以 ES5 风格的接口熟悉。文件顶部的注释如下所示,表示正在使用这个版本:

/*
 ECMAScript 5
 */

ECMAScript 2015

ECMAScript 2015 以前被称为 ECMAScript 6 (ES6)。这个版本于 2015 年完成,现在正在进入浏览器。它在所有主要浏览器的最新版本(Edge、Firefox、Chrome 和 Safari)中都有部分支持。一般来说,本书中编写的 JavaScript 代码(除了前面提到的例子)将使用 ES6。除了 React 部分,它使用了更高级的功能和一些 React 特定的扩展,其他使用的功能都在最新版本的 Chrome、Edge 和 Firefox 中得到支持。因此,如果你使用这些浏览器之一,你不必为这些示例实际运行转译器。如果你想将这些代码投入生产,那就是另一回事,超出了本书的范围。

文件顶部的注释如下所示,表示正在使用这个版本:

/*
 ECMAScript 6
 */

TypeScript

Angular (angular.io/) 部分将使用 TypeScript (www.typescriptlang.org/) 编写。TypeScript 是 JavaScript 的一个超集,它通过类型注解添加了某些可选功能,最显著的是静态类型化。TypeScript 被 Angular 团队用来为开发环境添加一些核心功能。因为并非每个人都有 TypeScript 的经验,所以示例中的 TypeScript 语言特性将被指出,以尽量减少混淆。

在这方面的好消息是,一旦脚本启动并运行,任何 Angular 组件的主体都可以用普通的旧 JavaScript 编写。

工具化

直到目前为止,我们在工具方面没有做太多工作。几乎所有的例子在本地文件系统上提供服务时都可以正常工作。

未来情况可能不会如此。在最简单的情况下,例如任何需要进行 HTTP 请求的示例,都将依赖于 node 包 serve (www.npmjs.com/package/serve) 来建立一个简单的本地服务器。

特别是 React 和 Angular 示例需要更广泛的工具。至少,您需要安装 Node.js(nodejs.org/en/),并且您需要按照一些步骤进行设置。最终,您将运行一个本地 Web 服务器,并且将有几个进程监视您的 JavaScript 或 Typescript 文件的更改。当您进行更改时,相关进程将捕捉更改并执行操作(例如将代码从 Typescript 转换为 JavaScript)以确保代码在本地服务器上更新。

每个相应部分都将提供有关如何使用代码示例的说明。

另外,请记住所有工作代码都可以在 GitHub 上找到(github.com/roblarsen/mastering-svg-code)。

在所有这些之后,让我们看一些不需要除了较新的 Web 浏览器之外的任何东西就可以在本地运行的代码。

SVG 的 DOM 接口

DOM 是用于访问、更新、创建和删除基于 XML 的文档的元素、属性和内容的 API。这包括相关但不严格符合 XML 语法的文档,例如最新的 HTML 规范。

对于普通开发人员来说,进行大量的纯 DOM 操作在今天是相当罕见的。多年前 jQuery 就解决了这个问题,而且从来没有再流行起来。我可以从经验中说,了解 DOM 操作的内部工作原理仍然很有用,这样当您遇到库或框架无法提供的东西时,您就可以自己编写代码来解决问题。

这也说明了在使用不同技术时可用的可能性。拥有对图书馆或框架作者感兴趣的东西的访问权限是一回事,但如果你熟悉底层代码,你只受你的想象力和目标浏览器中可用的东西的限制。

SVG DOM 基于 Dom Level 2 规范(www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html)。它支持大多数具有 DOM 和 HTML 经验的人所期望的内容,并添加了几组 SVG 特定接口,您可以使用这些接口来操作 SVG 文档。

本节将介绍 SVG 特定 DOM 方法的基本类,并说明它们的用法。除非您正在编写库,否则您不需要了解这些低级工具的所有内容。本章将作为一个介绍,让您对它们有一个良好的了解,并知道要寻找什么。

初始探索

要开始,让我们看一些 DOM 方法和属性,这些方法和属性可用于任意(常见的)SVG 元素rect。为此,您可以查看SVGRectElement元素文档(developer.mozilla.org/en-US/docs/Web/API/SVGRectElement)。那将是一个不错的选择。

您还可以直接检查rect元素,使用您选择的浏览器的开发人员工具。这将看起来像以下的屏幕截图。这将是您许多人接触 SVG 元素的可用方法和属性的方式:

虽然这些是常见的,但您也可以做一些像以下的事情,这是向脚本化 SVG 迈出的一大步。

在此代码示例中,我们使用document.getElementById访问rect元素,并将其存储在变量rect中。document.getElementById是您将用于访问 SVG 和 HTML 本身中的 DOM 元素的常见 DOM 访问器方法之一。您将在本章中看到其更多用法示例。

接下来,我们将通过简单的for...in循环遍历rect循环的属性,使用方括号表示法将变量和属性写入控制台,其中proprect元素上的属性或方法的名称:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- SVG Basic SVG DOM Manipulation</title>
</head>

<body>
    <svg  width="500" height="500"
     viewBox="0 0 500 500" version="1.1">
        <rect x="20" y="20" fill="blue" width="460" height="460"
         id="rect"></rect>
    </svg>
    <script>
    /*
        ES6
    */
        document.addEventListener("DOMContentLoaded",()=> 
            const rect = document.getElementById("rect");
            for (let prop in rect){
                let val = rect[prop];
                console.log(`${prop} = ${val}`);
            }
        });
    </script>
</body>

</html>

输出如下截图所示。您会注意到前几个属性和方法都是特定于 SVG 的。这个列表在下面的几个屏幕上继续,但列表中的第一个都是 SVG 特定的。这是因为for...in循环从SVGRectElement的最内部属性开始,然后沿着原型链向上工作,直到SVGElementElementNode的属性(最通用的 DOM 接口)。其中一些属性非常明显和立即有用,比如xywidthheight

其他可能不那么明显有用,比如getBBoxisPointInFill(尽管您可能能够猜到它们的作用),但您可以开始看到当您访问一个元素时,有很多可用的内容:

基于这个基本的基础和探索的想法,让我们开始构建一个小型应用程序,让您以简单的方式操作 SVG 画布。接下来的部分将重点介绍逐步构建一个小工具,允许您向 SVG 画布添加简单的 SVG 元素(textrectcircle)并以不同的方式操作它们。这个小演示将很容易理解,并将演示与 SVG 交互的许多不同方式。

SVG DOM 操作器

我们要构建的应用程序将允许您点击并向 SVG 画布添加三种不同类型的 SVG 元素。界面将允许您点击要添加的项目(rectcircletext),然后您将能够点击画布并将该元素添加到特定的(x,y)坐标处。选择该元素后,您将能够通过更改几个可用的属性来编辑它。

这个示例将使用 Bootstrap 来简化布局不同的表单字段,并创建一个简单的模态框来编辑属性。因此,jQuery 也将被包含在内,尽管至少在这个演示版本中,jQuery 的交互将被保持在最低限度;我们将专注于原始的 DOM 操作。

完成后,它将如下截图所示,显示了屏幕顶部的 SVG 画布,用黑色边框。之后是简单的说明,然后在屏幕底部有三个按钮,允许您选择要添加到画布的矩形、圆形或文本元素:

这一次,与其一次性添加整个代码示例并解释整个内容,不如我们在示例中构建并讨论每个代码块。

让我们从页面的骨架开始。这个初始状态完全没有任何 JavaScript,但它为我们提供了一些结构和一些稍后会用到的工具。

head中,我们从内容传送网络CDN)链接到 Bootstrap,从 Google 字体链接到 Raleway 字体,然后为我们的页面设置一些基本样式,将 Raleway 添加为正文字体,给我们的画布 SVG 元素加上边框,然后改变 SVG 精灵按钮的颜色。

在 body 中,我们使用 Bootstrap 的实用类来创建一个填满整个屏幕宽度的流体布局。SVG 元素将缩放以适应这个 Bootstrap 容器。

布局分为两部分:目标 SVG 元素,用于绘图的地方,和第二部分用于 UI 控件。目前,UI 控件只是包裹在 SVG 精灵周围的三个button元素。

接下来,我们有一个隐藏的 SVG 元素,其中包含一系列定义了我们精灵的symbol元素。

最后,我们链接到一些第三方 JavaScript,以便连接一些 Bootstrap 功能:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Basic The DOM Manipulator</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
  <link href="https://fonts.googleapis.com/css?family=Raleway" 
    rel="stylesheet">
  <style type="text/css">
    body {
      font-family: Raleway, sans-serif;
    }
    svg.canvas {
      border: 1px solid black;
    }

    button svg {
      fill: cornflowerblue;
      stroke: cornflowerblue;
      max-width: 50px;
    }
  </style>
</head>

<body>

  <div class="container-fluid">
    <div class="row">
        <div class="col-12">
            <svg  viewBox="0 0 500 
              200" version="1.1" id="canvas" class="canvas">
            </svg>
        </div>
    </div>
    <div class="row">
      <div class="col-5 offset-2">
        <h2>Pick an SVG element to add to the canvas. </h2>
        <p>Click on an item to select it and then click on the canvas
             to place it in the SVG element.</p>
      </div>
    </div>
    <div class="row">
      <div class="col-4 text-center">
        <button class="btn btn-link" title="click to add a circle">
          <svg  role="img">
            <use xlink:href="#circle"></use>
          </svg>
        </button>
      </div>
      <div class="col-4 text-center" title="click to add a square">
        <button class="btn btn-link">

          <svg  role="img">
            <use xlink:href="#square"></use>
          </svg>
        </button>
      </div>
      <div class="col-4 text-center">
        <button class="btn btn-link" title="click to add a text box">
          <svg  role="img">
            <use xlink:href="#text"></use>
          </svg>
        </button>
      </div>
    </div>
  </div>

  <svg  style="display:none">
    <defs>
      <symbol id="circle" viewBox="0 0 512 512">
        <circle cx="256" cy="256" r="256"></circle>
      </symbol>
      <symbol id="square" viewBox="0 0 512 512">
        <rect x="6" y="6" height="500" width="500"></rect>
      </symbol>
      <symbol id="text" viewBox="0 0 512 512">
        <rect x="6" y="106" height="300" width="500" fill="none" 
            stroke-width="10px"></rect>
        <text x="6" y="325" font-size="150">TEXT</text>
      </symbol>
      <!--
      Font Awesome Free 5.0.2 by @fontawesome - http://fontawesome.com
      License - http://fontawesome.com/license (Icons: CC BY 4.0,
         Fonts: SIL OFL 1.1, Code: MIT License)
      -->
      <symbol id="edit" viewBox="0 0 576 512">
          <title id="edit-title">Edit</title>
          <path d="M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 
            405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-
            92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-
            48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 
            0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 
            15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 
            8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 
            112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-
            48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-
            3.5 8.5z"></path>
        </symbol>
    </defs>
  </svg>
  <script>

  </script>
  <script src="img/jquery-3.2.1.slim.min.js"
     integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
    crossorigin="anonymous"></script>
  <script 
   src="img/>    per.min.js" integrity="sha384-
    ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
    crossorigin="anonymous"></script>
  <script 
   src="img/>    n.js" integrity="sha384-
    JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
    crossorigin="anonymous"></script>
</body>

</html>

现在我们已经了解了页面的基础知识,让我们开始添加一些交互性。

虽然页面上有 jQuery,但我不打算在任何 DOM 操作中使用它,这样我们就可以看一下原始的交互。我们将在第七章中看到 jQuery 和 SVG,所以不要担心。

我们要做的第一件事是创建一些事件处理程序来处理不同的交互。我们将添加的第一个事件处理程序是按钮上的click事件处理程序。想法是你点击按钮将一个 SVG 元素加载到你的光标上,然后再点击一次将其放置在画布上。这段代码还没有处理将元素添加到 SVG 画布中,但它确实展示了在处理 SVG 和 JavaScript 时的一些问题。

这是一个例子,有些你可能从老式 DOM 操作中知道的东西可能会让你失望。如果你一直在直接操作 HTML DOM,你可能习惯于使用Element.className属性。在 HTML 元素上,className属性是一个读/写字符串,对应于 HTML 元素上的class属性。在这种情况下,你可以操作字符串,改变会反映在 DOM 中。

DOM 接口SVGElement确实有一个className属性,但它不是一个简单的字符串。它是一个SVGAnimatedString属性,有两个字符串值,AnimValBaseVal。因为有了这一层额外的东西,而且因为我选择的替代接口现代化且更清晰,我决定使用SVGElement.classList属性来操作 CSS 类。classList是元素上 CSS 类的结构化接口。直接访问时,classList只读的,但有可用的方法来查询和操作类列表。

让我们深入了解一下这段代码是如何工作的。

我们通过添加一个在DOMContentLoaded事件上触发的函数来开始整个过程。这个事件在 DOM 被浏览器读取时触发一个函数。如果你想在浏览器读取标记时在页面上使用一个元素,这是开始操作 DOM 的最安全的地方。然后我们设置了两个本地引用,一个是通过变量doc引用document,另一个是通过canvas变量引用 SVG 画布本身。

我们创建本地引用 DOM 属性和元素,因为 DOM 查找可能很慢。保存本地引用 DOM 属性和元素是一种常见的性能模式。

然后我们使用querySelectorAll获取按钮的集合,并依次循环遍历每个按钮,为每个按钮添加一个点击事件处理程序。在点击事件处理程序的主体中,我们最初设置了两个本地引用,classlist是指向目标 SVG 元素的classList的引用,还有一个const,引用了被请求的元素的type。这个类型是通过use元素上的data-*属性传递的。data-*是一种在 DOM 元素上存储任意数据的方法。

然后我们使用该类型和一个简单的if...else语句来确保目标 SVG 元素上有适当的类。在第一个if块中,我们测试当前类是否与当前类型匹配,并且它具有active类。如果它们匹配当前类型并且元素具有活动类,我们将删除这些类。这个动作是为了在我们已经用特定类型加载了光标并且想要通过单击相同的按钮来重置它的情况。下一个块检查光标是否处于活动状态但不是当前选定的类型。在这种情况下,我们删除所有类型类以确保清除所选类型,然后再添加当前选定的类型。在最后一个块中,光标不活动,所以我们只是添加active类和类型类,加载光标:

    /*
    Ecmascript 6
    */
    document.addEventListener("DOMContentLoaded", () => {
      let doc = document;
      let canvas = doc.getElementById("canvas");
      doc.querySelectorAll(".controls .btn").forEach((element) => {
        element.addEventListener("click", (event) => {
          let classlist = canvas.classList;
          const type = event.srcElement.dataset.type;
          if (classlist.contains("active") && classlist.contains(type)){
            classlist.remove("active",type);
          }
          else if (classlist.contains("active")){
            classlist.remove("circle","text","square");
            classlist.add(type);
          } else {
            classlist.remove("circle","text","square");
            classlist.add("active",type);
          }
        });
      });
    });

活动光标的 CSS 如下。在新的 CSS 中,我们简单地为每个活动光标的光标属性传递了一个 PNG 的 URL 引用:

    svg.canvas.active.square{
      cursor:url(square.png), crosshair;
    }
    svg.canvas.active.circle{
      cursor:url(circle.png), crosshair;
    }
    svg.canvas.active.text{
      cursor:url(text.png), crosshair;
    }

加载了一个圆形元素的光标如下截图所示:

接下来,我们将逐步介绍在单击目标 SVG 元素时添加元素的过程。函数add是魔术发生的地方。首先我们设置了一些变量。我们首先使用五个常量。第一个是对document的引用,存储为doc,第二个是对目标 SVG 元素的引用,存储为canvas,第三个是目标 SVG 的classList,存储为classes,然后是 SVG 命名空间 URL 的引用,存储为namespaceNS),最后是创建并存储为pointSVGpoint。前三个应该很简单;NS变量的使用将很快解释。

point立即被使用。这是常规 DOM 操作和处理 SVG DOM 之间的一个主要区别,所以让我们来看看发生了什么。这段代码的基本目的是将点击事件的屏幕坐标转换为 SVG 元素内的正确(可能是变换或缩放后的)坐标。如果你一直在关注 SVG 的一般缩放方式以及变换如何与 SVG 元素一起工作的方式,你应该能够看到,根据文档的设置方式,屏幕像素可能与 SVG 文档中的用户单位匹配或不匹配。由于我们有一个静态的viewbox设置为 500 个用户单位,并且一个 SVG 元素被缩放以适应整个页面,我们需要使用一些 SVG 工具来访问当前的变换矩阵,并将该矩阵应用到点击的点上。

为了做到这一点,我们需要经历一些步骤。point是通过createSVGPoint创建的,这是一个返回当前 SVG 坐标系中点的方法。初始返回值有两个属性,xy,都设置为零。我们立即用点击事件的鼠标坐标填充该变量。这些坐标作为事件对象的一部分自动传递给函数作为event.offsetXevent.offsetY。接下来,我们使用getScreenCTM()方法来获取当前用户单位变换矩阵CTM)的逆。CTM 表示从屏幕坐标系转换到 SVG 文档中所需的变换步骤。调用inverse()方法返回从 SVG 用户单位坐标系转换到屏幕坐标系所需的步骤。因此,将该矩阵应用到 point 中定义的(x,y)点,将这些点移动到 SVG 文档中的正确位置。

最后,我们创建一个空变量elem,稍后将用要添加到文档中的元素填充。

接下来,我们实际创建元素。

如果目标 SVG 元素上有活动类,那么我们将向其添加一个元素。无论我们要创建哪种类型的元素,模式都是相同的:

  1. 我们测试活动元素的类型。

  2. 我们创建元素。

  3. 在将其添加到 DOM 之前,我们对其设置了一些属性。

再次,如果你熟悉 DOM 操作,你会注意到这里有一些不同。这就是NS变量发挥作用的地方。由于这不是纯 HTML,实际上是一个完全不同的文档定义,我们需要提供该命名空间以正确创建元素。因此,我们不是使用document.createElement,而是必须使用document.createElementNS,并通过NS变量引用 SVG 命名空间的第二个参数。

元素创建后,我们使用elem.setAttribute设置相关属性。对于rect,我们设置xywidthheight。对于circle,我们设置rcxcy。对于text元素,我们设置xy,然后使用elem.textContent设置文本内容,如果你习惯使用innerHTML更新文本和/或 HTML 节点,这是一个新的变化。正如之前提到的,SVG 元素没有innerHTML

一旦elem使用基线属性定义,我们就使用appendChild方法将其插入到文档中。最后,我们从目标 SVG 元素中删除"active"类,这将防止意外添加更多元素:

  function add(event) {
        const classes = canvas.classList;
        const NS = canvas.getAttribute('xmlns');
        const point = canvas.createSVGPoint()
        point.x = event.offsetX;
        point.y = event.offsetY;
        const svgCoords = 
        point.matrixTransform(canvas.getScreenCTM().inverse());
        let elem;
        if (classes.contains("active")) {
          if (classes.contains("square")) {
            elem = doc.createElementNS(NS, "rect");
            elem.setAttribute("x", svgCoords.x);
            elem.setAttribute("y", svgCoords.y);
            elem.setAttribute("width", 50);
            elem.setAttribute("height", 50);

          } else if (classes.contains("circle")) {
            elem = doc.createElementNS(NS, "circle");
            elem.setAttribute("r", 10);
            elem.setAttribute("cx", svgCoords.x);
            elem.setAttribute("cy", svgCoords.y);
          } else if (classes.contains("text")) {
            elem = doc.createElementNS(NS, "text");
            elem.setAttribute("x", svgCoords.x);
            elem.setAttribute("y", svgCoords.y);
            elem.textContent = "TEXT"
          }
          elem.setAttribute("fill", "#ff8000");
          canvas.appendChild(elem);
          classes.remove("active");
        }
      }

这是 SVG 画布上新添加的正方形元素如下:

虽然我们现在已经将事件绑定到文档并可以向屏幕添加元素,但这个演示还没有完成。我们需要做的是允许用户更新放置在目标 SVG 元素上的元素。虽然我们可以以越来越复杂的方式来做到这一点(点击和拖动,用鼠标或手指绘制新元素,复制和粘贴元素),但为了这个演示,我们只是允许用户点击 SVG 元素并打开一个小的 Bootstrap 模态框,让他们可以编辑基本的 SVG 属性。这将说明在不深入研究任何一组交互的情况下操纵底层 DOM 属性。这是特别重要的,因为许多最复杂的交互都最好由单独的库或框架处理。正如你将看到的,即使在最好的情况下,完全手工完成这些工作也可能很麻烦。

所以让我们开始吧。我们要做的第一件事是更新add函数的一行。这一行将点击事件处理程序添加到elem,这将触发edit函数。因此,看一下add函数底部,我们可以看到新代码:

          elem.setAttribute("fill", "#ff8000");
          canvas.appendChild(elem);
          classes.remove("active");  
 elem.addEventListener("click", edit, false);

在查看编辑功能之前,让我们先看一下模态框标记。如果你以前使用过 Bootstrap,这应该很熟悉。如果没有,基本知识是相当简单的。Bootstrap modal包装器类和modal-类的模式添加了 Bootstrap 模态框布局,并且这些类还指示 Bootstrap JavaScript 应该将 Bootstrap 特定的事件绑定到这个特定元素。我们很快将看到其中一个事件的作用。

每个模态框都有一个id,以便从我们的函数中引用,以及更新所选元素所需的特定表单字段。

第一个模态框用于编辑rect元素。它有一个color类型的input,允许用户选择新的背景颜色,两个number类型的input来更新xy坐标,以及两个number类型的input来更新元素的heightwidth

numbercolor类型的输入是较新的 HTML5 输入类型。

第二个模态框用于编辑circle元素。它提供了一个color输入来更改背景颜色,两个number输入来更改cxcy属性,以及一个最终的number输入来更改圆的半径。

最终的模态框用于编辑text元素。它提供了一个color input来改变文本的颜色,两个number inputs来改变元素的xy位置,以及一个text input来改变text元素的实际文本:

<div class="modal" tabindex="-1" role="dialog" id="rect-edit-modal">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Edit your element</h5>
          <button type="button" class="close" data-dismiss="modal"
             aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="row">
            <div class="col-4">
              <label for="rect-color">Background color:</label>
            </div>
            <div class="col-8">
              <input type="color" id="rect-color">
            </div>
          </div>
          <div class="row">
            <div class="col-2">
              <label for="rect-x">x:</label>
            </div>
            <div class="col-4">
              <input type="number" id="rect-x" class="form-control">
            </div>
            <div class="col-2">
              <label for="rect-y">y:</label>
            </div>
            <div class="col-4">
              <input type="number" id="rect-y" class="form-control">
            </div>
          </div>
          <div class="row">
            <div class="col-2">
              <label for="rect-width">width:</label>
            </div>
            <div class="col-4">
              <input type="number" id="rect-width" class="form-
                control">
            </div>
            <div class="col-2">
              <label for="rect-height">height:</label>
            </div>
            <div class="col-4">
              <input type="number" id="rect-height" class="form-
                control">
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" id="rect-
            save">Save changes</button>
          <button type="button" class="btn btn-secondary" data-
            dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>
  <div class="modal" tabindex="-1" role="dialog" id="circle-edit-
    modal">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Edit your element</h5>
          <button type="button" class="close" data-dismiss="modal" 
              aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="row">
            <div class="col-4">
              <label for="circle-color">Background color:</label>
            </div>
            <div class="col-8">
              <input type="color" id="circle-color">
            </div>
          </div>
          <div class="row">
            <div class="col-2">
              <label for="cirlce-cx">cx:</label>
            </div>
            <div class="col-4">
              <input type="number" id="circle-cx" class="form-control">
            </div>
            <div class="col-2">
              <label for="circle-cy">cy:</label>
            </div>
            <div class="col-4">
              <input type="number" id="circle-cy" class="form-control">
            </div>
          </div>
          <div class="row">
            <div class="col-2">
              <label for="circle-radius">radius:</label>
            </div>
            <div class="col-4">
              <input type="number" id="circle-radius" class="form-
                control">
            </div>

          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" id="circle-
            save">Save changes</button>
          <button type="button" class="btn btn-secondary" data-
            dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>
  <div class="modal" tabindex="-1" role="dialog" id="text-edit-modal">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Edit your element</h5>
          <button type="button" class="close" data-dismiss="modal"
             aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="row">
            <div class="col-4">
              <label for="text-color">Color:</label>
            </div>
            <div class="col-8">
              <input type="color" id="text-color">
            </div>
          </div>
          <div class="row">
            <div class="col-2">
              <label for="text-x">x:</label>
            </div>
            <div class="col-4">
              <input type="number" id="text-x" class="form-control">
            </div>
            <div class="col-2">
              <label for="text=y">y:</label>
            </div>
            <div class="col-4">
              <input type="number" id="text-y" class="form-control">
            </div>
          </div>
          <div class="row">
            <div class="col-2">
              <label for="text-text">content:</label>
            </div>
            <div class="col-10">
              <input type="text" id="text-text" class="form-control">
            </div>

          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" id="text-
            save">Save changes</button>
          <button type="button" class="btn btn-secondary" data-
            dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>

现在让我们来看一下edit函数。这里大部分有趣的事情都是基于event 参数event引用了有关触发的事件的各种信息。edit检查event.srcElement.nodeName来查看点击了什么类型的元素。然后,函数对每种元素类型都做三件事。

  1. 它使用带有"show"选项调用的$().modal方法打开正确的编辑模态框。

  2. 它使用 jQuery 的$().data()方法存储对当前元素的引用。$().data允许您将任意数据绑定到元素上。我们将在第七章中查看更多 jQuery 功能,常见的 JavaScript 库和 SVG,但由于我们已经在使用 jQuery 来获取 Bootstrap 方法,让我们在这里使用$().data()为了方便起见。

  3. 它从单击的元素中加载当前值并将其加载到表单字段中。这有多个实例,但在大多数情况下它们遵循相同的模式。form字段通过id引用,并使用event.srcElement.getAttribute访问的当前值进行设置。唯一的例外是通过event.srcElement.textContent属性访问的文本元素的文本值。

因此,一旦单击元素,模态框就会打开,并填充当前值,准备进行操作:

      function edit(event) {
        let elem = event.srcElement;

        if (event.srcElement.nodeName.toLowerCase() === "rect") {
          $("#rect-edit-modal").modal("show").data("current-element",
             elem);
          document.getElementById("rect-color").value = 
            elem.getAttribute("fill");
          document.getElementById("rect-x").value =
             elem.getAttribute("x");
          document.getElementById("rect-y").value = 
            elem.getAttribute("y");
          document.getElementById("rect-width").value =
             elem.getAttribute("width");
          document.getElementById("rect-height").value =
             elem.getAttribute("height");
        }
        else if (event.srcElement.nodeName.toLowerCase() === "circle") {
          $("#circle-edit-modal").modal("show").data("current-element",
             elem);
          document.getElementById("circle-color").value = 
            elem.getAttribute("fill");
          document.getElementById("circle-cx").value =
             elem.getAttribute("cx");
          document.getElementById("circle-cy").value = 
             elem.getAttribute("cy");
          document.getElementById("circle-radius").value =
             elem.getAttribute("r");
        }
        else if (event.srcElement.nodeName.toLowerCase() === "text") {
          $("#text-edit-modal").modal("show").data("current-element",
             event.srcElement);
          document.getElementById("text-color").value =
             elem.getAttribute("fill");
          document.getElementById("text-x").value =
             elem.getAttribute("x");
          document.getElementById("text-y").value = 
            elem.getAttribute("y");
          document.getElementById("text-text").value = 
            elem.textContent;
        }
      }

以下是打开的模态框的样子:

为了捕获更改,我们需要向文档添加一些更多的事件处理程序和一些更多的函数来保存数据。这是通过向三个模态框保存按钮添加一些点击处理程序,并定义三个不同的函数来处理更改来完成的。

正如您在下一个示例中所看到的,事件处理程序很简单。您可以使用document.getElementById获取每个保存按钮的引用,并使用addEventListener将正确的保存处理程序添加到每个元素中:

 document.getElementById("rect-save").addEventListener("click",
  rectSave);
 document.getElementById("circle-save").addEventListener("click",
 circleSave);
 document.getElementById("text-save").addEventListener("click", 
  textSave);

各种保存函数也很简单。它们最初都使用$.modal()方法隐藏打开的模态框,并传递hide参数。之后,函数使用$().data()方法的 get 签名存储对当前单击元素的引用,并将其存储为本地变量elem。然后,根据类型,函数从表单中访问值,并在所选元素上设置新值。rectSave访问fillxyheightwidth属性。circleSave访问fillcxcyr属性。text``Save访问fillxytext属性:

function rectSave() {
        $("#rect-edit-modal").modal("hide");
        let elem = $("#rect-edit-modal").data("current-element")
        elem.setAttribute("fill", document.getElementById("rect-
        color").value);
        elem.setAttribute("x", document.getElementById("rect-
        x").value);
        elem.setAttribute("y", document.getElementById("rect-
        y").value);
        elem.setAttribute("height", document.getElementById("rect-
        height").value);
        elem.setAttribute("width", document.getElementById("rect-
        width").value);
      }
      function circleSave() {
        $("#circle-edit-modal").modal("hide");
        let elem = $("#circle-edit-modal").data("current-element")
        elem.setAttribute("fill", document.getElementById("circle-
        color").value);
        elem.setAttribute("cx", document.getElementById("circle-
        cx").value);
        elem.setAttribute("cy", document.getElementById("circle-
         cy").value);
        elem.setAttribute("r", document.getElementById("circle-
        radius").value);
      }
      function textSave() {
        $("#text-edit-modal").modal("hide");
        let elem = $("#text-edit-modal").data("current-element")
        elem.setAttribute("fill", document.getElementById("text-
        color").value);
        elem.setAttribute("x", document.getElementById("text-
        x").value);
        elem.setAttribute("y", document.getElementById("text-
        y").value);
        elem.textContent = document.getElementById("text-text").value;
      }

text元素运行edit函数的效果如下截图所示:

应用这些值会产生以下输出:

虽然我们可以为这个小的 SVG 编辑演示添加许多更多的功能,但这个例子既足够简单,可以在一个章节中理解,也可以让我们说明用于在屏幕上添加、访问和更新 SVG 元素的基本模式。如果您以前有一些原始 DOM 操作的经验,这对您来说应该是很熟悉的。如果没有,这是一组有用的技能,您在这里看到的基本模式是 SVG 和 HTML 领域中所有工作的方式。一旦您访问了一个元素,您就可以访问和更新它的属性,并在其上调用各种方法来调整它在屏幕上的位置。有了这个基础,您将能够解决一些可能不容易通过库或框架解决的问题,无论是在 SVG 还是 HTML 中。

总结

在本章中,您创建了一个小型应用程序,允许您在目标 SVG 画布上添加和编辑 SVG 元素。通过这个应用程序,您学习了各种 DOM 功能和功能,包括:

  • 使用document.getElementByIddocument.querySelectorAll两种不同的方式访问 DOM 元素

  • 如何使用document.createElementNSappendChild插入 SVG 元素

  • 如何使用addEventListener将事件绑定到 HTML 和 SVG 元素

  • 如何使用classList接口从 SVG 元素获取、设置和移除 CSS 类

  • 如何使用getAttributesetAttribute操纵常见的 SVG 属性

  • 如何使用getScreenCTM方法在浏览器坐标系和 SVG 元素坐标系之间进行转换,以获取Current 用户单位* T *ransformation * M *atrix 的逆

  • 如何使用textContent设置 SVG 文本元素的文本内容

除了您已经在本书中学到的知识,本章学到的知识将使您能够在各种任务中以非常高的水平使用 SVG。如果您熟悉原始 DOM 接口,创建、访问和操作 DOM 元素的模式就是您构建最复杂的 Web 应用程序和可视化所需的一切。

在此基础上,我们将把到目前为止学到的所有知识应用到其他库和框架上,这样您就可以利用 jQuery、React 和 D3 等库以及 Angular 等框架在原始 DOM 接口之上提供的强大和便利功能。

第七章:常见的 JavaScript 库和 SVG

现在你已经看过了 SVG 的原始 DOM 接口,是时候看看 SVG 与一些更常见的 JavaScript 库和框架之间的接口了。借鉴于第六章中学到的JavaScript 和 SVG的经验,我们将研究在使用 jQuery、AngularJS(1.*)、Angular(2+)和 ReactJS 时使 SVG 正常工作时出现的一些特殊情况。这些示例不会很深入,但应该都能说明在处理 SVG 和这些其他代码库时存在的基本问题。这里的目标不是要完全向你介绍这些库和框架。只会有足够的介绍让你能够开始运行,然后每个部分都将处理该库或框架以及 SVG 的具体问题。

在本章中,我们将涵盖:

  • 使用广受欢迎的 jQuery 库和 SVG

  • Angular 1 和 Angular(2+)与 SVG 之间的接口

  • SVG 和 ReactJS,这是 Facebook 的流行库

使用 jQuery 操纵 SVG

我们将首先看的库是 jQuery。jQuery 并不像以前那样热门,但它仍然是地球上最流行的 JavaScript 库,了解在 SVG 中使用 jQuery 的特殊情况仍然可能是有用的。

由于 jQuery 作为常见 DOM 交互的友好替代,本节将展示我们在第六章中进行的 DOM 操作演示的基于 jQuery 的重写,JavaScript 和 SVG。

它使用完全相同的标记,因此在本章中我们需要查看的唯一位置是底部的脚本块。

此代码将以惯用的 jQuery/ES5 编写。

我们将首先看一下我们将在 jQuery 的DOMContentLoaded事件的等价事件上触发的函数,即$(document).ready()$(document).ready()接受一个函数作为参数。正如其名称所示,当文档的 DOM 内容加载完成时,该函数将被执行。

虽然你可以传入一个函数表达式,但我们将定义一个传递给$(document).ready()的传统命名的函数init

在其中,我们设置了一些事件处理程序。第一个是我们按钮的click事件处理程序。它触发loadCursor函数。第二到第四个事件处理程序为每种不同的 SVG 元素类型创建save事件。最后一个将add函数添加到#canvas元素中,以便在画布元素上单击时知道要将所选的 SVG 元素放到页面上:

  function init() {
      $(".controls .btn").on("click", loadCursor); 
      $("#rect-save").on("click", rectSave);
      $("#circle-save").on("click", circleSave);
      $("#text-save").on("click", textSave);
      $("#canvas").on("click", add);
    }
$().ready(init);

现在我们已经看过了启动应用程序的函数,让我们依次看看其他函数。首先我们将看看add函数的新版本。add有一个主要的问题,然后还有几个较小的问题。

我们首先通过获取一个加载了 jQuery 引用的$("#canvas") SVG 元素来开始。之后,初始化与函数的纯 JavaScript 版本类似。

这包括一个主要的问题,即 jQuery 的预期行为失败的地方。虽然常见的 jQuery 元素创建方法如$("<rect>")适用于 SVG 元素,并将<rect>元素插入页面,但它们仍然需要使用正确的命名空间进行创建。没有命名空间,就像你在上一章中学到的那样,它们将被浏览器视为任意的 HTML 元素,并不会按预期渲染。因此,就像纯 JS 示例中一样,我们需要向元素创建添加命名空间。因此,我们使用与仅 JavaScript 示例中相同的elem = doc.createElementNS(NS, "rect");模式来执行此操作。一旦元素被创建,它就可以像通常一样被插入到 DOM 中并用 jQuery 进行操作。

元素创建后,squarecircletext的各个选项都与仅 JavaScript 示例类似地处理。在这种情况下,我们只是使用 jQuery 的便利方法$().hasClass()$().attr()来测试类名并设置各种属性。

最后,我们使用更多的 jQuery 便利方法将元素添加到$canvas元素中,移除"active"类,并添加click事件处理程序来编辑元素:

function add($event) {
      var $canvas = $("#canvas");
      var elem;
      var doc = document;
      var NS = canvas.getAttribute('xmlns');
      var point = canvas.createSVGPoint();
      var $elem;
      point.x = $event.offsetX;
      point.y = $event.offsetY;
      var svgCoords = 
        point.matrixTransform(canvas.getScreenCTM().inverse());
      if ($canvas.hasClass("active")) {
        if ($canvas.hasClass("square")) {
          elem = doc.createElementNS(NS, "rect");
          $elem = $(elem).attr({
            "x": svgCoords.x,
            "y": svgCoords.y,
            "width": 50,
            "height": 50
          });

        } else if ($canvas.hasClass("circle")) {
          elem = doc.createElementNS(NS, "circle");

          $elem = $(elem).attr({
            "cx": svgCoords.x,
            "cy": svgCoords.y,
            "r": 10
          });
        } else if ($canvas.hasClass("text")) {
          elem = doc.createElementNS(NS, "text");
          $elem = $(elem).attr({
            "x": svgCoords.x,
            "y": svgCoords.y,
            "width": 50,
            "height": 50
          });
          $elem.text("TEXT");

        }
        $elem.attr("fill", "#ff8000");
        $canvas.append($elem);
        $canvas.removeClass("active");
        $elem.on("click", edit);
      }
    }

三个编辑函数再次遵循与普通 JS 示例相同的模式。在每个函数中,我们获取一个加载的 jQuery 引用到target元素,并将其存储为$elem。然后我们使用 jQuery 方法$().prop,它查找对象属性,以测试调用对象的nodeName。然后我们显示正确的模态,使用 Bootstrap 模态方法调用"show"参数,并使用 jQuery $().data方法设置当前元素。$().data,正如你在第六章中记得的,JavaScript 和 SVG,在元素上获取和设置任意数据。然后我们使用$().val()方法的组合,它获取或设置表单输入的值,和$().attr()方法,它获取或设置元素属性,来填充表单值。$().val()在这里用于通过读取 SVG 元素的值来设置表单的值,使用$().attr()调用getter(没有参数)并将该值作为$().val()的参数:

   function edit($event) {
      var $elem = $($event.target);
      if ($elem.prop("nodeName") === "rect") {
        $("#rect-edit-modal").modal("show").data("current-element",
         $elem);

        $("#rect-color").val($elem.attr("fill"));
        $("#rect-x").val($elem.attr("x"));
        $("#rect-y").val($elem.attr("y"));
        $("#rect-width").val($elem.attr("width"));
        $("#rect-height").val($elem.attr("height"));
      }
      else if ($elem.prop("nodeName") === "circle") {
        $("#circle-edit-modal").modal("show").data("current-element",
         $elem);
        $("#circle-color").val($elem.attr("fill"));
        $("#circle-cx").val($elem.attr("cx"));
        $("#circle-cy").val($elem.attr("cy"));
        $("#circle-radius").val($elem.attr("r"));
      }
      else if ($elem.prop("nodeName") === "text") {
        $("#text-edit-modal").modal("show").data("current-element",
         $elem);
        $("#text-color").val($elem.attr("fill"));
        $("#text-x").val($elem.attr("x"));
        $("#text-y").val($elem.attr("y"));
        $("#text-text").val($elem.text());
      }
    }

最后,我们有各种save方法。这些遵循与之前示例相同的模式。这与普通 JS 示例的基本工作流程相同,但我们再次能够使用完整的 jQuery 便利方法来操作我们的 SVG 元素:使用 Bootstrap 方法隐藏模态,使用$().data()方法获取对当前元素的引用,然后使用$().attr()方法设置属性,称为setter,和$().val()称为getter,作为参数:

    function rectSave() {
      $("#rect-edit-modal").modal("hide");
      var $elem = $("#rect-edit-modal").data("current-element");
      $elem.attr({
        "fill": $("#rect-color").val(),
        "x": $("#rect-x").val(),
        "y": $("#rect-y").val(),
        "height": $("#rect-height").val(),
        "width": $("#rect-width").val()
      });
    }
    function circleSave() {
      $("#circle-edit-modal").modal("hide");
      var $elem = $("#circle-edit-modal").data("current-element");
      $elem.attr({
        "fill": $("#circle-color").val(),
        "cx": $("#circle-cx").val(),
        "cy": $("#circle-cy").val(),
        "r": $("#circle-radius").val()
      });
    }
    function textSave() {
      $("#text-edit-modal").modal("hide");
      var $elem = $("#text-edit-modal").data("current-element");
      $elem.attr({
        "fill": $("#text-color").val(), "x": $("#text-x").val(),
        "y": $("#text-y").val()
      });
      $elem.text($("#text-text").val());
    }

正如你所看到的,除了元素创建之外,使用 SVG 和 jQuery 是直接的。元素创建需要使用标准 DOM 方法,但与 SVG 元素的其他交互可以使用适当的 jQuery 方法。

使用 AngularJS 和 SVG

现在是时候看看在更完整的应用程序框架中使用 SVG。我们将从 AngularJS 开始,这是 Google 广受欢迎的应用程序框架的原始版本。虽然 AngularJS(Angular 1.*)在 Web 框架的背景下已经过时,但它仍然受欢迎,并在许多环境中使用。它也为许多人所熟悉,并且被广泛部署,因此从多个角度来看,了解如何在 AngularJS 应用程序中使用 SVG 是有用的。

这个和接下来的示例将比 jQuery 和纯 JavaScript 演示更简单。这有两个原因。首先,你已经在 SVG 和 JavaScript 在 DOM 中的交互方面看到了很多细节。你实际上已经准备好自己处理 SVG DOM 操作,因此在不同框架中涵盖大量变化可能甚至不那么有益。覆盖基础知识应该足够让你自己去做。

其次,我们不希望太多关于实际库和框架的细节。将每个介绍保持在最低限度意味着我们可以专注于讨论的 SVG 部分。为此,我们将看看最简单的演示,它将展示在应用程序中使用元素的两个最重要方面:将动态 SVG 元素插入 DOM,并通过用户交互对其进行操作。

演示将如下所示:

这段代码将以惯用的 ES5 方式编写。

以下是代码。这个示例的所有代码都在一个单独的 HTML 文件中。这通常不是您构建 AngularJS 应用程序的方式,但对于这个示例来说,它完全可以。

文档head使用必要的脚本和样式设置应用程序。我们链接到 Bootstrap,jQuery 和 Angular:

<head>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
  <link rel="stylesheet" href="style.css" />
  <script src="img/jquery-3.3.1.min.js" 
          integrity="sha256-
            FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
            crossorigin="anonymous"></script>
  <script  
   src="img/>    min.js"></script>
</head>

有趣的部分从body元素开始。这是我们设置 Angular 应用程序的地方。ng-app属性表示 Angular 应该处理body元素及其所有子元素,并将 Angular 的特殊解析规则应用于其中包含的标记。我们很快会看到ng-app"angularSVG"的引用指的是什么。

接下来的标记是我们将 UI 与 Angular 功能和功能绑定在一起的地方。Angular 使用特殊属性和自定义 HTML 元素的组合来创建动态界面。

从我们的角度来看,最重要的部分是使用ng-attr前缀来处理fillcxcyr属性。Angular 允许您在标记中引用当前控制器作用域中的变量,只要它包含在{{}}模式中,Angular 就会用模型中的值替换该引用。这是一个实时引用,它将在常规周期中自动更新。

这个非常方便的特性与某些 SVG 属性兼容。虽然在您玩转应用程序并将值从 Angular 令牌转换为数值后,以下内容最终会起作用,但在文档加载时会出现错误:

<circle
      fill="{{fill}}" 
      cx="{{cx}}" 
      cy="{{cy}}" 
      r="{{r}}" />

错误可以在以下截图中看到。SVG 解析器期望一个length值,而实际上得到的是一个字符串:

修复这个问题需要使用ng-attr前缀。这个前缀告诉 Angular 在插值步骤中使用allOrNothing标志。简单来说,这意味着如果属性的值是undefined,则不会将属性呈现到文档中。一旦它有一个值,它就会像正常一样呈现。

这个标记的第二个有趣部分是自定义 HTML 元素angular-rectangular-rect是 Angular 中所谓的指令。指令是 Angular 用来创建自定义 HTML 元素和属性的机制,允许您通过自己设计的可重用代码块来扩展和增强常见的 HTML 元素和文档。虽然这个很简单,但很快你会看到,这个自定义元素将简洁地说明 Angular 指令如何与 SVG 一起工作。

标记中唯一有趣的部分是使用ng-model属性将 JavaScript 变量值绑定到表单字段中。这个特殊的 AngularJS 属性在标记和 Angular 控制器之间建立了双向数据绑定。我们很快会看到这些变量是如何在控制器中设置的,但请记住一点,一旦建立了这种连接,AngularJS 会保持它的活力,并在form字段更新时自动更新 SVG 元素中的值:

<body ng-app="angularSVG">
  <div ng-controller="circleController" class="container">
    <svg  width="150" height="150" 
     viewBox="0 0 150 150" version="1.1">
      <circle
      ng-attr-fill="{{fill}}" 
      ng-attr-cx="{{cx}}" 
      ng-attr-cy="{{cy}}" 
      ng-attr-r="{{r}}" />
      <angular-rect></angular-rect>
    </svg>
    <div class="row">
      <div class="col-4">
        <label>Background color:</label>
      </div>
      <div class="col-8">
        <input type="color" ng-model="fill" id="circle-color">
      </div>
    </div>
    <div class="row">
      <div class="col-2">
        <label>cx:</label>
      </div>
      <div class="col-4">
        <input type="number" ng-model="cx" id="circle-cx" class="form-
          control">
      </div>
      <div class="col-2">
        <label>cy:</label>
      </div>
      <div class="col-4">
        <input type="number" ng-model="cy" id="circle-cy" class="form-
         control">
      </div>
    </div>
    <div class="row">
      <div class="col-2">
        <label>radius:</label>
      </div>
      <div class="col-4" height="{{cx}}>
        <input type="number" ng-model="r" id="circle-radius" 
          class="form-control">
      </div>
    </div>
  </div>

JavaScript 非常简单。只需几行 JavaScript 代码,就可以将表单字段的值动态调整为圆的高度、宽度和填充颜色。第一部分是angular.module()方法调用,创建了一个名为"angularSVG"的 Angular 应用程序。这个引用是 Angular 在标记中寻找的,以便知道页面上是否有一个 Angular 应用程序。如果它在ng-app中找到这个值,它会解析该标记并将基于 Angular 的魔术应用到页面上。

接下来是我们小的控制器定义,circleControllercircleController有一个参数,即 Angular 的$scope变量。如果您对 Angular 不熟悉,可以将$scope视为函数的this值的受控别名。它是控制器的内部状态,$scope中的属性和方法对 JavaScript 代码和对 Angular 感知的标记都是可用的。

在控制器内部,我们只是在$scope上设置了一些变量。这些变量作为圆的基线值,并且由于它们绑定到 Angular 的$scope,它们自动成为与圆和表单字段中相应值的活动、双向链接。

之后,我们创建了一个简单的 Angular 指令angularRect,它只是在 SVG DOM 中插入一个rect元素。我们不会在这里讨论 Angular 指令的复杂性,但有一个特定的细节对于 SVG 元素非常重要。返回对象的templateNamespace属性指示 Angular 应将该指令视为 SVG。没有它,就像 jQuery 的常见 DOM 创建模式和 DOM 方法document.createElement一样,该指令将被插入文档,但它不会被创建为一个正确的 SVG 元素。它会存在,但在渲染时不会显示为一个正方形:

Angular 在 JavaScript 中使用友好的驼峰命名法,然后在将元素插入文档时使用短横线命名法。

 <script>
    angular.module('angularSVG', [])
      .controller('circleController', function ($scope) {
        $scope.cx = 75;
        $scope.cy = 75;
        $scope.r = 50;
        $scope.fill = "#ff0000";
      }).directive('angularRect', function() {
        return {
            restrict: 'E',
            templateNamespace: 'svg',
            template: '<rect x="125" y="125" width="10" height="10"
             stroke="blue" fill="none"></rect>',
            replace: true
        };
});
  </script>

在浏览器中运行并调整数值后,效果如下截图所示。初始截图显示了加载初始数值的演示:

第二个截图显示了调整后的数值和圆形元素相应地发生了变化:

现在我们已经了解了 AngularJS,让我们来看看 Angular 的新进化,Angular 2.0+。这个版本的框架被称为 Angular,与之前的版本非常不同,功能非常强大。

让我们快速看一下。

使用 Angular 操作 SVG

从 AngularJS 转向,让我们来看看 Angular 的现代进化。Angular 2.0+(简称为 Angular)是一个非常现代的框架。它通常是用 TypeScript 编写的,这是 JavaScript 的一个超集,它添加了一些可选功能,Angular 利用这些功能为库添加了一些非常方便的功能和功能。

开始使用 Angular

由于 Angular 是一个较新的框架,占用的空间较大,我们将介绍一些设置步骤。下载示例中的代码将可以工作,但了解如何到达那里是非常有用的。所以,让我们开始设置。

这个 Angular 示例将复制使用 Angular 代码重新制作的与 AngularJS 示例提供的完全相同的演示。正如您可能已经感觉到并将继续学习的那样,无论您使用哪种库或框架,动态 SVG 的基本问题都是相同的;解决方案只是略有不同。

您可以使用任何您喜欢的文本编辑器来进行 Angular 示例,但我建议使用微软的 VS Code。它是免费的,得到很好的支持,经常更新,并且与 TypeScript 非常兼容。

安装 Node、npm 和 Angular Cli

在您开始使用 Angular 之前,您需要设置好实际运行代码所需的工具。一切的基础是 Node.js 和 Node 的包管理器npm。因此,如果您还没有安装,您应该首先安装它们。最简单的方法是转到nodejs.org并下载适用于您操作系统的安装程序。

安装完成后,您可以继续安装 Angular 的命令行工具CLI)。Angular CLI 使得启动 Angular 项目变得非常容易,您很快就会看到。以下命令将在您的计算机上全局安装 Angular CLI:

  1. 安装完成后,使用ng new命令创建一个项目。ng new将创建一个新的文件夹,其中包含启动 Angular 项目所需的一切。我们不会详细介绍,但运行此命令后,您应该已经准备好开始使用您的应用程序了:

  1. 下一步是进入您刚创建的文件夹并运行npm install

npm install将确保所有依赖项都安装在node_modules中,并且您的应用程序将准备就绪。

  1. 从 VS Code 的以下屏幕截图显示了初始化应用程序并运行npm install 后的布局:

  1. 由于我们在这个演示版本中也使用 Bootstrap,因此需要确保它可用。通过运行以下命令完成:
npm install --save bootstrap 

这将把 Bootstrap 安装到您的node_modules中:

然后,您可以在angular-cli.json中进行连接。angular-cli.json是您配置 Angular CLI 安装的不同方面的地方。在这种情况下,我们只需要将 Bootstrap CSS 添加到 styles 属性中,以便它将与应用程序的其余部分捆绑在一起:

在幕后,Angular CLI 使用 Webpack 来捆绑脚本和样式,并以多种方式处理它们,以便将它们准备好交付到开发服务器以及生产环境。使用 Angular CLI 的最大好处之一是它简化了使用 Webpack 的复杂性。Webpack 非常强大,但学习曲线陡峭。Angular CLI 让它变得简单易用。

 "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "../node_modules/bootstrap/dist/css/bootstrap.css",
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],

在这种简单状态下运行应用程序将允许我们开始开发应用程序,并针对在本地运行的开发服务器进行测试。这是使用ng serve命令完成的。在编译代码后,使用--open选项将打开一个 Web 浏览器:

这将在浏览器中自动重新加载代码,每当对代码进行更改时。

因此,现在是时候开始编写一些 TypeScript 并与 SVG 进行交互了。

我们要做的第一件事是编辑应用程序的主模块。app.module.ts是应用程序的根模块,它是应用程序的所有部分连接在一起的地方。大部分都是由Angular CLI自动连接的。我们只需要使用新的 ES6 模块模式(import module from src)从 Angular 核心导入FormsModule。然后将其添加到@NgModule装饰器的imports数组中。这允许FormsModule的指令和属性在此应用程序中可用:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
    AngularRectComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

接下来,我们将完全编辑app.component.ts文件以表示我们的(简单)组件。在其中,我们从 Angular 导入ComponentFormsModule,在@Component装饰器中进行一些标准的维护工作,然后导出AppComponent类,其中包含四个设置的属性。这种模式值得一些解释,因为它可能很熟悉,但又有足够的不同之处,可能会让人费解。首先,所有这些都是使用public关键字创建的。这表示这些属性应该在类的范围之外可用。接下来是变量名称本身,后跟冒号和类型注释,指示变量的预期类型。TypeScript 允许您基于其他 TypeScript 类创建自定义类型,但对于我们的目的,我们只是使用标准的 JavaScript 原语,numberstring。最后,我们为它们设置默认值,以便我们的应用程序有东西可以依靠:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public cx:number = 75;
  public cy:number = 75;
  public r:number = 50
  public color:string = "#cc0000";
}

接下来是标记,与之前的示例类似。它都包含在app.component.html中。与 AngularJS 版本有一些相似之处。例如,动态属性必须以类似的方式处理,仍然不能直接绑定到 SVG 属性而不引起错误,因此仍然必须显式地管理它们。在这种情况下,您使用attr.前缀而不是在 AngularJS 中使用的ng-attr-前缀。您还会注意到属性周围的方括号。

使用简单的方括号[]表示这是单向数据绑定;模板从我们之前定义的组件属性中读取。稍后,在输入中,我们看到了使用方括号/括号[()]语法围绕属性的显式双向数据绑定的示例。ngModel是我们使用FormsModule导入的指令。它允许我们从表单元素到组件属性进行双向数据绑定。这样,表单中的条目再次表示为 SVG circle元素的属性,并且随着对form字段的更改而显示更改。

<div class="container">
  <svg  width="150" height="150" viewBox="0 0 150 150" version="1.1">
    <svg:circle
    [attr.fill]="color"
    [attr.cx]="cx"
    [attr.cy]="cy"
    [attr.r]="r" />
  </svg>
  <div class="row">
    <div class="col-4">
      <label>Background color:</label>
    </div>
    <div class="col-8">
      <input type="color" [(ngModel)]="color" id="circle-color">
    </div>
  </div>
  <div class="row">
    <div class="col-2">
      <label>cx:</label>
    </div>
    <div class="col-4">
      <input type="number" id="circle-cx" [(ngModel)]="cx" class="form-
        control">
    </div>
    <div class="col-2">
      <label>cy:</label>
    </div>
    <div class="col-4">
      <input type="number" id="circle-cy" [(ngModel)]="cy" class="form-
        control">
    </div>
  </div>
  <div class="row">
    <div class="col-2">
      <label>radius:</label>
    </div>
    <div class="col-4">
      <input type="number" id="circle-radius" [(ngModel)]="r" 
        class="form-control">
    </div>
  </div>
</div>

我们只需要做一件事情,就可以使这个 Angular 示例与之前的 AngularJS 示例匹配,那就是添加一个代表小蓝色rect元素的子组件。这里有一些有趣的地方。首先是展示了 Angular CLI 的强大之处。使用 Angular CLI,如果需要连接一个组件,可以使用ng new命令。在我们的例子中,我们将运行ng new component angular-rect,这将生成组成 Angular 组件的各种文件,并将实际将组件连接到app.module.ts中:

您可以在以下更新的代码示例中看到app.module.ts的样子,其中导入了新的AngularRectComponent组件并将其添加到@NgModule声明中:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AngularRectComponent } from './angular-rect/angular-rect.component';

@NgModule({
  declarations: [
    AppComponent,
    AngularRectComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

还有一些与 SVG 直接相关的问题,需要指出才能将这个自定义元素放到页面上。首先是需要在angular-rect组件中的元素中添加svg:前缀。这告诉 Angular,您猜对了,它应该在创建这些元素时使用 SVG 命名空间:

<svg:rect x="125" y="125" width="10" height="10" stroke="blue" fill="none"></svg:rect>

下一个问题是一个两部分的问题。对于由简单 HTML 元素组成的组件,您可以像这样做,这与您在 AngularJS 中看到的类似。您可以按照以下方式将元素添加到页面中:

<angular-rect></angular-rect>

这将在 Web 检查器中的实时视图中呈现如下:

<angular-rect _ngcontent-c0="" _nghost-c1=""><rect _ngcontent-c1="" fill="none" height="10" stroke="blue" width="10" x="125" y="125"></rect>
</angular-rect>

从标记的角度来看,这看起来很好,但在浏览器中,蓝色矩形消失了。整个元素没有渲染,即使它在 DOM 中。

在 HTML5 中,这种做法可以工作,因为 HTML5 解析器已经被设计成对未知元素(以及格式不正确的标记)宽容,并且您可以使用 CSS 操作自定义元素。另一方面,SVG 仍然是严格的 XML 语法,因此除非元素在 SVG 规范中,或者您可以指向定义该特定元素的基于 XML 的文档类型定义DTD),否则它不会正确渲染。幸运的是,有一个与 Angular 组件的功能完全兼容的 SVG 形状解决方案。您可以使用 Angular 绑定自定义组件到g元素的能力来创建几乎相同的效果。

以下代码示例显示了如何做到这一点。

首先,让我们看看angular-rect组件本身。需要注意的是,大部分文件都是样板文件,唯一需要注意的是@Component装饰器中的选择器被包裹在方括号[]中。由于它被包裹在方括号中,这告诉解析器它是一个属性选择器,而不是您在应用程序组件本身中看到的常见元素选择器。这意味着 Angular 将查找元素的属性中是否存在angular-rect,并将其替换为我们的新自定义组件:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: '[angular-rect]',
  templateUrl: './angular-rect.component.html',
  styleUrls: ['./angular-rect.component.css']
})
export class AngularRectComponent implements OnInit {

  constructor() {}

  ngOnInit() {}

}

接下来,我们将看到如何在标记中使用。我们再次将svg:前缀添加到g元素,然后我们只需添加angular-rect属性,组件就会正确渲染:

  <svg  width="150" height="150" viewBox="0 0 150 150" version="1.1">
    <svg:circle
    [attr.fill]="color"
    [attr.cx]="cx"
    [attr.cy]="cy"
    [attr.r]="r" />
    <svg:g angular-rect></svg:g>
  </svg>

Angular 到此为止。

使用 React 和 SVG

我们要看的最后一个库是 React。React 是一个非常流行的库,它在 AngularJS 变得陈旧之际出现,而在 Angular 准备好投入使用之前出现。在某些圈子里非常受欢迎。它基于 ES6,并具有一些特定于 React 的扩展。

其中许多内容对你来说可能很熟悉,仅仅基于你在本章中迄今所看到的内容,特别是如果你做过任何严肃的 Web 应用程序开发。

开始使用 React 并不像使用 Angular 那样直接。Angular 在内部可能更复杂,但 Angular CLI 消除了许多问题,因此作为开发人员,你几乎不会(或很少)看到复杂性。React 更像是一个库而不是一个完整的框架,因此为了启动和运行,你可能需要做出更多的决定。幸运的是,虽然有许多方法可以实现这一点,但没有任何方法像 Angular CLI 对 Angular 那样对项目至关重要(它们在文档和社区中紧密耦合),但有一些方法可以像 Angular CLI 一样简单地实现。也许甚至更简单,因为根本不需要安装任何东西。

假设你的机器上安装了 Node 版本>6,你只需要运行一个命令就可以创建演示代码中使用的简单应用程序:

$ npx create-react-app react-svg 

create-react-app是 Facebook 的一个实用工具,可以启动一个完全功能的 React 应用程序。运行它看起来像以下两个屏幕截图(完整滚动将占据书的许多页面)。

这很酷。它创建文件夹,下载所有的依赖项并安装所有内容,然后给你一系列命令,以便与你新创建的 React 应用程序进行交互:

持续结果:

更深入地看,它创建了一个看起来像以下屏幕截图的目录。它包含了node_modules文件夹和所有的依赖项,public是编译后文件的存放位置(当你浏览你的工作代码时,它们是从这里提供的),src是你的应用程序的所有源文件的存放位置。其他文件都是git/npm/yarn-based project的标准文件:

Yarn 是npm的替代品。我们不会详细介绍两者之间的区别,因为这超出了本书的范围,而且说实话,也不是很有趣。可以说,yarn 是npm的并行工具,因此你将使用 yarn 做与npm相同的事情。语法偶尔有所不同,在运行yarn install时会创建一个不同的文件(yarn.lock)。但就本书而言,你不需要关心这些区别。

如前所述,应用程序代码在src中。你可以在以下屏幕截图中看到该文件夹的布局。

App.cssApp.jsApp.test.js是你的应用程序的核心所在。index.js文件是你的应用程序的主要入口点,它会引导你的 React 应用程序。registerServiceWorker.js是框架提供的一个文件,用于从本地缓存中提供资源。但是,在这个简单的应用程序中,你实际上不会碰它:

从项目文件夹的根目录运行yarn start将编译所有的 React 代码和 CSS,并将启动一个可在 localhost:3000访问的开发服务器:

启动应用程序如下,以防你想知道。我们将很快消除它:

在我们开始深入研究 SVG 和 React 之前,让我们看一下create-react-app生成的基本 React 组件。你之前已经看到它的渲染;现在让我们看看它是如何工作的。

React 组件的基本格式如下。它是一个 ES6 模块,带有import语句、一个类和一个导出。有一个特定于 React 的元素值得注意。

文件顶部显示了 ES6 导入。这可以包括 CSS 文件(我们马上就会看到)和 SVG 图像。Webpack 实际上会读取这些导入语句并优化这些导入,就像 Webpack 与 Angular 装饰器一样工作。

接下来是文件中的唯一一个类。App,它扩展自 React 的基本Component类。它有一个方法render(),它使用了一种称为 JSX 的 JavaScript 扩展。JSX 允许您将 XML 和 JavaScript 混合在一起。老实说,我从来不太喜欢这种格式,当他们发布它时我几乎感到震惊,但我已经开始欣赏它的意图,即使我不喜欢它。如果 JSX 属性被引用,则它们被解析为字符串。否则,它们被视为 JavaScript 表达式。在这种情况下,logo.svg的路径被转换为有用的路径,并在浏览器中呈现出 logo。

最后,我们导出默认类App,其他应用程序可以导入它:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to 
            reload.
        </p>
      </div>
    );
  }
}

export default App;

快速浏览一下index.js,因为我们实际上没有做太多事情,这将展示应用程序如何加载。

文件顶部有几个 ES6 模块导入。React 和 ReactDOM 是核心,驱动基本的 React 库并添加 ReactDOM 接口。它们主要驱动我们在这个小演示中要做的大部分工作。

导入还包括index.css文件。

除此之外,我们还导入了两个 JavaScript 模块:App,这是我们要进行工作的模块,以及之前提到的registerServiceWorker,我们将完全不使用它。

一旦所有内容都被导入,我们运行两个小函数。ReactDOM.render被调用时带有两个参数,<App />表示由 App 组件创建的自定义元素,document.getElementById("root")表示应接收新元素的节点:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

现在我们已经快速浏览了基本的 React 架构,让我们来看看我们的演示。

除了基本的 React 应用程序之外,Bootstrap 也通过运行以下命令安装到该项目中。我们将继续使用相同的标记来做另一个简单的表单/SVG 演示,这是有道理的:

npm install --save bootstrap

让我们看看我们的App.js。它以几个导入语句开始。我们从 React 中导入Reactcomponent。然后我们导入两个 CSS 文件,我们自己的自定义App.css和刚刚安装的Bootstrap CSS,链接到项目的node_modules中的文件。最后,我们从rect模块导入我们独立的ReactRect组件。

然后是App类的定义。它实际上只有几件事情。在构造函数中,我们创建一个基本的state对象,其中包含我们标准 SVG 属性cxcyrcolor的默认值。然后我们设置一个方法handleChange来处理对底层模型的更改。

这个方法很简单。它接收event对象,创建一个target常量,然后进一步检查该对象以获取输入的namevalue。然后它使用setState方法(从props继承)来设置应用程序状态的值。

接下来是render函数。

浏览一下,您会注意到您不需要做太多工作就可以让 React 正确地呈现 SVG。

首先,我们使用 ES6 解构赋值模式为各种属性设置本地变量。一旦这些变量设置好了,只需将需要由 React 解释的变量添加到适当属性的大括号{}中。SVG 元素和表单输入中的变量引用以相同的方式处理,不需要任何特殊处理。

我们只需将handleChange方法直接绑定到标记中的onChange事件,一切都会如预期般运行。

我们导入的ReactRect被添加到 SVG 元素中。React 负责导入该组件,我们很快就会看到它,并将其呈现到文档中。

自定义组件需要以大写字母开头。以小写字母开头的标记被解释为 HTML 元素。

import React, { Component } . from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.css';
import ReactRect from './rect';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      cx: 75,
      cy: 75,
      r: 50,
      color: "#cc0000"
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    const target = event.target;
    const value = target.value;
    const name = target.name;
    this.setState({
      [name]: value
    });
  }
  render() {
    const { cx,cy,r,color } = this.state;
    return (
      <div className="container">
      <svg  width="150" height="150" 
        viewBox="0 0 150 150" version="1.1">
        <circle
        r={r}
        cy={cy}
        cx={cx}
        fill={color}
        ></circle>
        <ReactRect></ReactRect>
      </svg>
      <div className="row">
        <div className="col-4">
          <label>Background color:</label>
        </div>
        <div className="col-8">
          <input type="color" id="circle-color" value={color}
          name="color"
          onChange={this.handleChange} />
        </div>
      </div>
      <div className="row">
        <div className="col-2">
          <label>cx:</label>
        </div>
        <div className="col-4">
          <input type="number" id="circle-cx" className="form-control" 
            value={cx}
          name="cx"
          onChange={this.handleChange} />
        </div>
        <div className="col-2">
          <label>cy:</label>
        </div>
        <div className="col-4">
          <input type="number" id="circle-cy" className="form-control" 
            value={cy}
          name="cy"
          onChange={this.handleChange} />
        </div>
      </div>
      <div className="row">
        <div className="col-2">
          <label>radius:</label>
        </div>
        <div className="col-4">
          <input type="number" id="circle-radius" className="form-
            control" value={r}
          name="r"
          onChange={this.handleChange} />
        </div>
      </div>
    </div>
    );
  }
}

export default App;

我们的自定义元素非常简单。它只是一个简单的 React 组件,返回我们的rect元素:

import React, { Component } from 'react';

class ReactRect extends Component {
  render() {
    return (
      <rect x="125" y="125" width="10" height="10" stroke="blue" 
        fill="none"></rect>

    );
  }
}

export default ReactRect;

正如您所看到的,使用动态 SVG 和 React 非常简单。React 团队努力确保 SVG 元素和属性都能正常工作,因此这归功于他们的辛勤工作。谢谢,React 团队!

总结

在本章中,您将使用四个常见的库和框架,将这些强大的工具与 SVG 集成在一起。

从 jQuery 开始,通过 AngularJS、Angular 和 React,您现在具有将 SVG 与地球上四个最受欢迎的库和框架之一集成的基本经验。

具体来说,您学习了如何使用每个框架设置应用程序,如何创建具有 SVG 元素和属性的动态组件,以及如何以动态方式操纵这些属性。

您还了解了在使用 SVG 和这些库时的多个注意事项,包括确保元素在 jQuery 中正确创建以及确保在 Angular 框架中正确处理动态属性的方法。

第八章:SVG 动画和可视化

这一章讨论了 SVG 的最具动态和令人印象深刻的用例:使用 SVG 进行数据可视化和动画。您已经了解的工具,SVG、JavaScript 和 CSS,以及一些新工具,将汇集在一起,为您构建动态站点和应用程序提供强大的选择。

在这一章中,我们将学到以下内容:

  • 如何使用 SVG、JavaScript 和结构化数据生成静态数据可视化

  • 动画 SVG 的一般技术概述

  • 使用 Vivus 对 SVG 进行动画处理

  • 使用 GSAP 进行动画

在完成本章中的示例后,您将能够使用 SVG 创建动画和数据可视化,并了解使用 SVG 和动画的两种最佳工具。

让我们开始吧。

创建 SVG 数据可视化

这一部分将专注于使用 SVG 和 JavaScript 组合基本数据可视化。这个特定的可视化将关注一个插图,即相对于平均值的正/负差异。在这种情况下,它将说明棒球选手大卫·奥尔蒂兹在波士顿红袜队生涯中每个赛季击出的本垒打数量与他在红袜队生涯中的平均本垒打数量的比较。

从 2003 年到 2016 年,大卫·奥尔蒂兹在为红袜队效力期间,每个赛季最少击出 23 个本垒打,最多击出 54 个。他的平均每个赛季 34.5 个。这个可视化将展示他每年本垒打总数相对于 34.5 平均值的正/负差异。他击出比平均值多的年份将以绿色显示。击出比平均值少的年份将以红色显示。

我们需要经历的步骤如下:

  1. 我们将获取数据并计算总年数、总本垒打数量,然后计算平均值。

  2. 我们将循环遍历数据,并计算每年的正/负偏移量。

  3. 我们将根据可用的屏幕空间计算一些指标。

  4. 我们将在屏幕上垂直居中绘制一个基准线。

  5. 我们将在适当的位置绘制一系列矩形,其高度适当以指示正/负的差异,以及一些简单的标签指示年份和本垒打的数量。

  6. 我们将添加一个图例,指示本垒打的平均数量和年数。

最终的可视化将如下所示:

现在我们已经计划好了基础知识,让我们详细看看这是如何工作的。

我们将从标记开始,这非常简单。我们首先包括 Bootstrap 和 Raleway 字体作为我们标准模板的一部分。然后,我们设置 SVG 元素的背景,并设置两种不同类型文本元素的字体系列、大小和颜色。然后我们只需包括目标 SVG 元素和运行可视化的 JavaScript 文件:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Data Visualization</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
  <link href="https://fonts.googleapis.com/css?family=Raleway" 
    rel="stylesheet">
  <style type="text/css">
    body {
      font-family: Raleway, sans-serif;
    }
    svg.canvas {
     background: #0C2340;
    }
    text {
      font-family: Raleway, sans-serif;
      font-size: .75em;
      fill: #fff;
    }
    text.large {
      font-size: 1.5em;
    }
  </style>
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450" 
          width="1000" height="450" version="1.1" id="canvas" 
            class="canvas">
        </svg>
      </div>
    </div>
  </div>
  <script src="img/scripts.js"></script>
</body>

</html>

包含的 JavaScript 文件是真正的工作所在。

这个 JavaScript 文件使用了几个 ES6 特性。

scripts.js本身基本上是一个大函数,viz

viz的顶部,我们有data变量。这个变量是一个 JavaScript 对象数组。每个对象都有两个属性,yearhrs,表示相关年份和奥尔蒂兹在那一年击出的本垒打数量:

function viz() {
  /*
    ES6
  */
  const data = [
    {
      "year": 2003,
      "hrs": 31
    },
    {
      "year": 2004,
      "hrs": 41
    },
    {
      "year": 2005,
      "hrs": 47
    },
    {
      "year": 2006,
      "hrs": 54
    },
    {
      "year": 2007,
      "hrs": 35
    },
    {
      "year": 2008,
      "hrs": 23
    },
    {
      "year": 2009,
      "hrs": 28
    },
    {
      "year": 2010,
      "hrs": 32
    },
    {
      "year": 2011,
      "hrs": 29
    },
    {
      "year": 2012,
      "hrs": 23
    },
    {
      "year": 2013,
      "hrs": 30
    },
    {
      "year": 2014,
      "hrs": 35
    },
    {
      "year": 2015,
      "hrs": 37
    },
    {
      "year": 2016,
      "hrs": 38
    }
  ];

如果您正在交互式地运行此可视化,要么接受用户的输入,要么将 Web 服务调用的结果插入到可视化中,您只需要具有正确的结构(对象数组)和格式(hrsyear),其他一切都会自动完成。在查看填充文件的其余变量和方法时,请记住这一点。

data开始,我们设置了几个不同的变量,我们将在可视化过程中使用,除了data之外:

  • doc:对文档的引用

  • canvas:引用具有id#canvas的 SVG 元素

  • NS:从SVG元素派生的命名空间的引用

  • elem:我们将创建的元素的占位符变量

  const doc = document;
  const canvas = doc.getElementById("canvas");
  const NS = canvas.getAttribute('xmlns');
  let elem;

接下来是我们用来填充可视化值和元素的几个实用方法。

第一个函数addText让我们可以向可视化添加文本标签。它接受一个坐标对象coords,要输入的text,最后是一个可选的 CSS 类cssClass。我们将在一个示例中探讨 CSS 类参数的用例。前两个参数应该很简单,是必需的。

addText之后,有一个addLine函数,它允许我们在屏幕上绘制线条。它接受一个坐标对象coords(在这种情况下包含四个坐标)和一个可选的stroke颜色。您会注意到stroke在函数签名中创建了一个默认值。如果没有提供描边颜色,stroke将是#ff8000

接下来是addRect函数,它允许我们向屏幕添加矩形。它接受一个坐标对象coords,其中包含heightwidth属性,以及可选的strokefill颜色。

最后,有一个函数maxDiffer,它计算出一组正负数之间的最大差值。获取这个范围,然后使用这个最大差确保无论数字如何分布,基线上方或下方所需的最大高度都能适应屏幕:

  function addText(coords, text, cssClass) {
    elem = doc.createElementNS(NS, "text");
    elem.setAttribute("x", coords.x);
    elem.setAttribute("y", coords.y);
    elem.textContent = text;
    if (cssClass){
      elem.classList.add(cssClass);
    }
    canvas.appendChild(elem);
  }
  function addLine(coords, stroke = "#ff8000") {
    elem = doc.createElementNS(NS, "line");
    elem.setAttribute("x1", coords.x1);
    elem.setAttribute("y1", coords.y1);
    elem.setAttribute("x2", coords.x2);
    elem.setAttribute("y2", coords.y2);
    elem.setAttribute("stroke", stroke);
    canvas.appendChild(elem);
  }
  function addRect(coords, fill = "#ff8000", stroke = "#ffffff") {
    elem = doc.createElementNS(NS, "rect");
    elem.setAttribute("x", coords.x);
    elem.setAttribute("y", coords.y);
    elem.setAttribute("width", coords.width);
    elem.setAttribute("height", coords.height);
    elem.setAttribute("fill", fill);
    elem.setAttribute("stroke", stroke);
    canvas.appendChild(elem);
  }
  function maxDiffer(arr) {
    let maxDiff = arr[1] - arr[0];
    for (let i = 0; i < arr.length; i++) {
      for (let j = i + 1; j < arr.length; j++) {
        if (arr[j] - arr[i] > maxDiff) {
          maxDiff = arr[j] - arr[i];
        }
      }
    }
    return maxDiff;
  }

在这些实用函数之后,我们有定义可视化核心的代码。它发生在一个在DOMContentLoaded事件上运行的函数中。

当函数运行时,我们创建多个变量,保存我们需要生成可视化的不同属性。以下是它们的作用:

  • viewBox是 SVG 元素viewBox的本地引用。我们将这个和后续的 DOM 引用存储在本地,这样我们就可以节省viewBox的 DOM 查找次数。

  • width是对 SVG 元素viewBox中宽度的本地引用。

  • height是对viewBoxheight的本地引用。

  • x是对viewBoxx点的本地引用。

  • y是对viewBoxy点的本地引用。

  • padding是一个任意的常数,用于创建几个填充计算。

  • vizWidth定义了 SVG 画布的可见宽度。这定义了我们可以安全地将元素绘制到 SVG 元素中的区域。

  • years是数据集中的年数的引用。

  • total是一个计算出的值,代表整个数据集中击出的全垒打总数。

  • avg是每年击出的全垒打的平均数,通过将total除以years得出。

  • verticalMidPoint表示 SVG 元素的垂直中点。这是正负差异绘制的基准线。

  • diffs是一个数组,保存了每年击出的全垒打平均数和实际击出的全垒打数之间的正负差异。

  • maxDiff是每年击出的全垒打的平均数和实际击出的全垒打数之间的最大差异。

  • yInterval是每个全垒打的像素数。这确保了方框在垂直方向上根据每年击出的全垒打数正确地进行缩放。

  • xInterval是每年的像素数。这个值允许我们均匀地在 SVG 元素中放置方框,无论数据集中有多少年:

  document.addEventListener("DOMContentLoaded", () => {
    const viewBox = canvas.viewBox.baseVal;
    const width = viewBox.width;
    const height = viewBox.height;
    const x = viewBox.x;
    const y = viewBox.y;
    const padding = width / 200;
    const vizWidth = width - padding;
    const years = data.length;
    const total = data.reduce((total, item) => {
      return total + item.hrs;
    }, 0);
    const avg = total / years;
    const verticalMidPoint = (y + height) / 2;
    const diffs = data.map((item) => {
      return item.hrs - avg;
    });
    const maxDiff = maxDiffer(diffs);
    const yIntervals = verticalMidPoint / maxDiff;
    const xInterval = (vizWidth / years);

在创建所有这些变量之后,我们开始绘制不同的框并添加标签。为此,我们使用for...in循环来循环遍历diffs数组,进行两个计算以创建两个新变量newXnewYnewX是基于i的值乘以我们之前创建的intervalX变量的常规间隔。newY变量是通过将diffs[i]的值(当前差异)乘以yInterval常量来计算的。这为我们提供了一个距离,用于计算矩形的高度,以表示每年的本垒打数量。

接下来,我们测试当前diff是否大于或小于零。如果大于零,我们希望绘制一个从verticalMidPoint向上的框。如果当前diff小于零,则我们绘制一个从verticalMidPoint向下的框。由于矩形的方向和相关的锚点在每种情况下都不同,我们需要以不同的方式处理它们。我们还将使用不同的颜色来突出显示这两种变化,以便进行次要指示。

虽然这个if的两个分支之间存在差异,但两个分支都调用了addRectaddText。让我们看看if的两个分支之间的相似之处和差异之处。

首先,每次调用addRect都遵循相同的模式,对于xwidth属性。x始终是newX值加上padding,而widthxInterval值加上padding

yheight值由两个分支处理。

如果当前差异小于零,则新的y坐标为verticalMidpoint。这将使框的顶部锚定在可视化中表示零的线上,并指示框将悬挂在该线下方。如果当前差异大于零,则y坐标设置为verticalMidPoint减去newY。这将使新矩形的顶部值为newY在表示零的线上方。

如果当前差异小于零,则height是传入Math.abs()newY值。无法向 SVG 元素传递负值,因此需要使用Math.abs()将负值转换为正值。如果当前差异大于零,则height就是newY值,因为它已经是正数。

if的每个分支中调用addText的位置不同。如果newY值为负数,则再次使用Math.absnewY值转换为正数。否则,保持不变。

随后,我们使用addLine调用将零线添加到垂直中点。传入的参数是viewBox的未更改的xwidth,左右两个点的verticalMidpint作为y值。

最后,我们添加了一些解释可视化基础知识的文本。在这里,我们使用了cssClass参数可选项来调用addLine,传入large,以便我们可以制作稍大一些的文本。xy参数利用了xheight变量以及padding变量,将文本放置在 SVG 元素的左下角略微偏移。

最后一行代码只是调用viz()函数来启动可视化。

for (const i in diffs) {
      const newX = xInterval * i;
      const newY = diffs[i] * yInterval;
      if (diffs[i] < 0) {
        addRect({
          "x": newX + padding,
          "y": verticalMidPoint,
          "width": xInterval - padding,
          "height": Math.abs(newY),
        }, "#C8102E", "#ffffff");
        addText({
          "x": newX + padding,
          "y": verticalMidPoint + Math.abs(newY) + (padding * 3)
        }, `${data[i].hrs} in ${data[i].year}`);
      }
      else if (diffs[i] > 0) {
        addRect({
          "x": newX + padding,
          "y": verticalMidPoint - newY,
          "width": xInterval - padding,
          "height": newY,
        }, "#4A777A", "#ffffff");
        addText({
          "x": newX + padding,
          "y": verticalMidPoint - newY - (padding * 2)
        }, `${data[i].hrs} in ${data[i].year}`);
      }
      addLine({
        x1: x,
        y1: verticalMidPoint,
        x2: width,
        y2: verticalMidPoint
      }, "#ffffff");
      addText({
        "x": x + padding,
        "y": height - (padding * 3)
      }, `Based on an average of ${avg} home runs over ${years} years`,
            "large");
    }
  });

}
viz();

如果这是一个用于生产或更通用用途的可视化,那么我们仍然需要对其进行一些处理。敏锐的读者会发现,我们实际上并没有处理本垒打数量恰好等于平均本垒打数量的情况,例如。也就是说,对于本书的目的,这里的细节足以说明如何使用 JavaScript、SVG 和数据来以可视化的方式讲述数据集的故事。

现在我们已经看过静态可视化了,让我们来看一下如何在屏幕上添加一些动作。下一节将介绍在浏览器中可以对 SVG 进行动画的多种方式。

动画 SVG 的一般技术

本节将介绍各种用于动画 SVG 的一般技术。虽然有不同的工具可用于完成这项工作(您将在本章后面遇到两种),但了解在没有框架或库的帮助下如何完成这些工作是很有用的。本节将提供这方面的基础知识。

您之前已经看到了一些这些技术,但是在动画的上下文中再次查看它们是很好的。

使用纯 JavaScript 进行动画

在 CSS 关键帧动画和 CSS 过渡出现之前,我们不得不手动使用 JavaScript 在浏览器中制作所有动画和有趣的效果;在循环中更新属性并手动优化帧速率。最终,诸如 jQuery 之类的库出现并消除了对了解这些工作原理的需求,通过将动画作为其 API 的一部分呈现出来。幸运的是,如今,除了您选择的工具中可用的动画方法之外,您还可以利用 CSS 动画来完成许多以前需要使用 JavaScript 的事情,因此现在越来越少需要人们学习这些技能。

也就是说,有些地方 CSS 动画无法胜任,因此了解它在幕后如何工作并且不依赖库是有好处的。

这个简单的动画将会将一个圆形元素从左到右地在 SVG 元素上移动。我们需要计算几个指标来创建动画,所以即使它很简单,它也会说明你在编写这种代码时可能遇到的许多挑战。

让我们来看一下代码。

head中没有任何有趣的内容,所以让我们直接跳到页面的bodybody中有我们在整本书中一直使用的标准 Bootstrap 标记。在主div内部,我们有一个包含单个circle元素的SVG元素,位于75, 225,半径为50像素。它的idcircle

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450" 
          width="1000" height="450" version="1.1" id="canvas" 
           class="canvas">
          <circle cx="75" cy="225" r="50" fill="blue" id="circle">
        </circle>
        </svg>
      </div>
    </div>
  </div>

JavaScript 很简单。

它包括一个添加到DOMContentLoaded事件的函数。该函数执行一些熟悉的操作。它创建了对doccanvascircle的本地引用,以便我们可以在整个动画过程中轻松地引用这些元素。接下来创建了几个变量来存储viewBox的属性:viewBox本身,heightwidthx。然后设置了两个常量,表示动画运行的秒数和我们动画的每秒帧数fps)。

接下来,我们将当前圆形元素的x值作为变量currX。然后计算结束点newX,通过使用圆的半径乘以 3 来计算。这给我们一个舒适的结束点,视觉上。

接下来,我们创建一些变量来运行动画。首先,diffX是当前x值和目标newX值之间的差值的计算。然后我们将diffX除以秒数,再乘以每秒帧数。这将创建三秒的间隔来进行动画。

最后,我们创建了动画变量animX,这是我们在每一帧中将要处理的变量,因为我们要将元素动画移动到屏幕上。

接下来,有一个函数会在每一帧调整元素在屏幕上的位置。它有三个作用。它将间隔添加到animX上,以通过计算的间隔移动元素。然后设置元素的cx属性,将其移动到新位置。最后,使用window.requestAnimationFrame递归调用自身。

requestAnimationFrame是一种允许浏览器优化 JavaScript 动画绘制到屏幕上的方法。它通常优化到每秒60帧,但从技术上讲,它将匹配设备的显示刷新率。

所有这些都发生在一个if块内,当动画完成时停止动画。如果animX小于newX,则执行代码,再次调用animate来启动下一帧。如果animX大于或等于newX,则动画停止:

document.addEventListener("DOMContentLoaded", () => {

 const doc = document;
 const canvas = doc.getElementById("canvas");
 const circle = doc.getElementById('circle');
 const viewBox = canvas.viewBox.baseVal;
 const width = viewBox.width;
 const height = viewBox.height;
 const x = viewBox.x;
 const padding = width / 200;
 const seconds = 3;
 const fps = 60;
 let currX = circle.cx.baseVal.value;
 let newX = width - (circle.r.baseVal.value * 3);
 let diffX = newX - currX;
 let intervalX = diffX / (fps * seconds);
 let animX = currX;
 function animate() {
    if (animX < newX) {
        animX = animX + intervalX;
        circle.setAttribute("cx", animX);
        window.requestAnimationFrame(animate);
     }
 }
 animate();
 });

这不是最复杂的动画,但使用window.requestAnimationFrame意味着在浏览器中看起来相当不错。

虽然有其他选项可以对 SVG 进行动画,你应该了解它们并在适当的地方使用它们,但 JavaScript 将是最强大且最灵活的选择。如果你的动画需要在尽可能多的浏览器中运行,那么你需要使用 JavaScript。

好消息是,正如你将在本章后面看到的,有很好的工具可以简化使用 JavaScript 进行动画。

在我们看几个用于处理 SVG 的 JavaScript 库之前,让我们看一下使用核心 Web 技术对 SVG 进行动画的另外两个选项:CSS 和 SMIL。

使用 CSS 进行动画

使用 CSS 对 SVG 进行动画是直接的,它的工作方式与 CSS 动画和过渡与常规 HTML 元素的工作方式相同。你定义一些 CSS 属性,根据你是否使用关键帧动画或过渡,你创建特定的 CSS 规则来处理它们在一段时间内的渲染。这个过程的问题在于,只有演示属性,它们驱动了 SVG 的许多内容,也可以作为 CSS 属性使用,才能用 CSS 进行操作。正如你在下面的网站上看到的,根据 SVG 1.1 的定义,缺少许多重要的属性:www.w3.org/TR/SVG/propidx.html。SVG 2.0 添加了更多属性,但对这些新属性的支持并不是普遍的,不幸的是,没有一个适当的手册来说明哪些属性在哪里得到支持。

换句话说,根据你的浏览器支持矩阵的情况,使用这些技术可能会有一些潜在的问题。

无论如何,即使有这样一个有些粗糙的故事,看到这些技术的实际应用仍然是值得的。

这里有三个例子。两个显示了类似于之前的 JavaScript 动画的动画;它们将一个蓝色圆圈移动到屏幕上。它们以两种不同的方式实现。这说明了根据你的目标浏览器的不同,你可能会看到实现上的差异。第一个例子使用 CSS 变换和 CSS 动画来将元素沿屏幕移动。这种技术具有更广泛的浏览器支持。第二个例子使用更简单的方法,在 SVG 元素悬停时设置cx属性的过渡,然后更改值。在 Chrome 中,cx作为 CSS 属性可用,因此在该浏览器中,这是更简单的方法。

第三个例子展示了元素上fill的过渡,以说明在这种情况下将计算留给浏览器和 CSS 是非常有益的一个例子。如果不清楚如何从一个颜色值动画到另一个颜色值,那么你可能至少能看到一个将繁重的工作留给浏览器的绝佳用例。

让我们按顺序看一下这些例子。

第一个例子很简单。在这个例子中,我们有与之前的 JavaScript 例子相同的标记,只有一个例外:通过 CSS 设置cx属性。我们在文档的head中的#circle选择器中这样做。

此外,我们在该选择器上设置了一个transition属性,监视cx属性的变化,并在其变化时进行三秒的过渡。在下一个选择器svg:hover #circle中,我们通过父 SVG 元素上的悬停事件触发动画,将cx值设置为最终目的地875像素。

有了这个 CSS,当你在 SVG 元素上悬停鼠标时,新的cx被设置,浏览器将在 SVG 元素的X轴上在75875像素之间进行动画处理:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with CSS</title>
  <link rel="stylesheet" 
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.m
    in.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
  <style type="text/css">
    #circle {
      transition: cx 3s;
      cx: 75px;
    }
    svg:hover #circle {
      cx: 875px;
    }
  </style>

</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450"
         width="1000" height="450" version="1.1" id="canvas" 
          class="canvas">
          <circle cy="225" r="50" fill="blue" id="circle"></circle>
        </svg>
      </div>
    </div>
  </div>

</body>

</html>

下一个例子设置类似。它与前一个例子具有完全相同的 SVG 标记,并由 JavaScript 进行动画处理。区别再次在于 CSS。

有两个感兴趣的部分。第一部分定义了一个名为animate-circle的两关键帧动画。第一个关键帧,在0%处,使用transform: translateXX轴上进行0px的平移。第二个关键帧,在100%处,将该变换增加到800px

然后,在#circle选择器中,我们使用命名动画定义animation属性,持续时间为三秒,线性缓动。然后我们将animation-fill-mode设置为 forwards,表示动画应该向前运行一次并完成,保持动画元素处于最终状态。

当这个运行时,圆圈会平滑地在屏幕上进行动画处理:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG CSS Animation</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
  <style type="text/css">
    @keyframes animate-circle {
      0% {
        transform: translateX(0)
      }
      100% {
        transform: translateX(800px)
      }
    }

    #circle {
      animation: animate-circle 3s linear;
      animation-fill-mode: forwards;
    }
  </style>

</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450" 
         width="1000" height="450" version="1.1" id="canvas" 
          class="canvas">
          <circle cx="75" cy="225" r="50" fill="blue" id="circle">
        </circle>
        </svg>
      </div>
    </div>
  </div>

</body>

</html>

最后一个例子也使用了过渡,这次是将fill属性从蓝色动画到红色。这个属性是早期在 CSS 中定义为可用的演示属性之一,因此在当前时间,它在浏览器中的支持要比cx等属性好得多。

CSS 定义非常简单。在#circle定义上设置了一个fill属性,以及一个transition,用于监视fill的变化,并在 2 秒内进行过渡。

#circle:hover中,我们将fill更改为蓝色。在浏览器中运行并悬停在圆圈上,将会使圆圈元素的颜色进行动画处理,而无需使用任何 JavaScript,并且无需弄清楚如何从一个命名颜色动画到另一个。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Data Visualization</title>
  <link rel="stylesheet" 
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.m
  in.css" integrity="sha384-
  Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
  <style type="text/css">
    #circle {
      fill: red;
      transition: fill 3s;
    }
    #circle:hover {
      fill: blue;
    }
  </style>
</head>
<body>
  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 450 450"
         width="450" height="450" version="1.1" id="canvas" 
            class="canvas">
          <circle cx="225" cy="225" r="225" fill="blue" id="circle">
        </circle>
        </svg>
      </div>
    </div>
  </div>
</body>
</html>

所有这些例子都是故意基本的,正如前面提到的,它们的浏览器支持很弱(例如,在 IE 旧版本中都不起作用);但它们仍然很强大。如果你的浏览器支持矩阵偏向于最新和最好的浏览器,那么你可以在 CSS 和 SVG 中玩得很开心。

使用 SMIL 对 SVG 进行动画处理

SVG 动画的另一个有趣且强大的选项与 CSS 具有类似令人沮丧的支持矩阵。SMIL 在 Microsoft 浏览器中根本不受支持,甚至曾一度被 Chrome 弃用。

这是一件遗憾的事,因为 SMIL 有一些很好的特点。它是一种清晰的、声明式的动画元素的方式。它不像 JavaScript 那样强大,也不像 CSS 那样常用作通用技术,但它仍然相当不错。

看一个例子。

在其中,我们有我们现在熟悉的标记:一个简单的circle在一个空的 SVG 元素上。这次有一个小小的变化。animate元素作为circle元素的子元素。animate元素是动画定义的地方。它有几个属性,我们需要看一下:

  • xlink:href属性指向将要进行动画处理的#circle元素。animate元素是circle元素的子元素,这样就自动将动画与其关联起来。使用xlink:href属性可以确保连接被准确定义。

  • attributeName定义将要进行动画处理的属性。在这种情况下,它是cx属性。

  • fromto属性表示动画的起点和终点。在这种情况下,我们将从"75"移动到"900"

  • dur指示动画的持续时间。在这种情况下,它被定义为"3s",持续三秒。

  • begin属性指示动画应该何时开始。这让你可以根据需要延迟动画。在我们的例子中,我们立即开始动画,设置为"0s"

  • fill属性,与常见的fill属性同名,表示动画值在动画结束后是否保留在元素上。这个值"freeze"表示元素应该在动画结束时保持在达到的状态上。

在 SVG 的上下文中,似乎没有很好的理由将fill重载为执行两个基本不相关的任务。这很不幸。

在浏览器中运行这个动画会创建一个类似于本章中几个实例中看到的动画;球从左边开始,在三秒内移动到右边。

<!doctype html>
<html lang="en">

<head>
 <meta charset="utf-8">
 <title>Mastering SVG- SVG Animation with SMIL</title>
 <link rel="stylesheet" 
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.m
    in.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>

<body>

 <div class="container-fluid">
 <div class="row">
 <div class="col-12">
 <svg  viewBox="0 0 1000 450" 
    width="1000" height="450" version="1.1" id="canvas" class="canvas">
    <circle cx="75" cy="225" r="50" fill="blue" id="circle">
 <animate 
 xlink:href="#circle"
 attributeName="cx"
 from="75"
 to="900" 
 dur="3s"
 begin="0s"
 fill="freeze" />
 </circle>
 </svg>
 </div>
 </div>
 </div>
</body>

</html>

现在我们已经看了 SVG 中数据可视化和动画的手动方法,让我们来看一些可以帮助动画元素的工具。

使用 Vivus 对 SVG 进行动画

Vivus 是一个只做一件事情并且做得非常好的库(maxwellito.github.io/vivus/)。Vivus 允许您在一段时间内“绘制”SVG 元素的描边。

以下一系列截图展示了它的效果。这是一个很好的效果。

需要注意的是,本章中的三个示例都使用了相同的插图。书中打印的代码示例截断了每个路径元素的d属性,以缩短代码示例的长度。如果您想看完整的示例,请参考 GitHub 上的代码(github.com/roblarsen/mastering-svg-code)。 

只要stroke设置了一个值并且fill设置为none,只需包含 Vivus JavaScript 文件(在这种情况下,我们通过在 Vivus 文件夹中运行npm install,然后链接到node_modules文件夹中的 JavaScript 文件来实现),然后创建一个新的 Vivus 实例就可以了。

创建一个新的 Vivus 实例非常容易。使用new关键字,您可以用两个参数实例化一个新的 Vivus 对象。第一个是 SVG 元素的id。第二个是配置对象。在这种情况下,我们只传入一个选项,即duration参数,将动画的持续时间设置为三秒(3,000 毫秒)。

以下代码示例展示了使用 Vivus 有多么容易:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with Vivus</title>
  <style>
  .stroke{
    stroke-linejoin: round;
  }
  </style>
</head>

<body>

 <div class="container-fluid">
 <div class="row">
 <div class="col-12">
 <svg id="loader"  viewBox="0 0 
    250.23 131.83"><title>Logo</title><path fill="none" stroke="#000"
     d="M160.9,26.9l-.37.25c6.81,8.24,10.62,17.49"/>
<path fill="none" stroke="#000" 
 d="M28.14,92.59c1.43,1.56,2.81,3,4,4.45,3.56,4.31,6.05"/>
<path fill="none" stroke="#000" d="M80.3,57.58c.27,4.74.54,9.34.81,14l-
 19.33,1v8.1a4.56,4.56,"/>
<path fill="none" stroke="#000" 
 d="M160.9,26.9a5.89,5.89,0,0,1,1.08.74c11.41,"/>
<path fill="none" stroke="#000" d="M28.14,92.59c-3.72,5.21-7.28,"/>
<path fill="none" stroke="#000" 
 d="M80.3,57.58,59.18,59.36V56.54h21C79.42,"/>
<path fill="none" stroke="#000" 
 d="M43.87,73.26a5.31,5.31,0,0,1-.24,5.8c-1.51-.76-1.58-.91-1-2.4Z"/><path fill="none" stroke="#000" d="M103.13,55.28,90"/></svg>
 </div>
 </div>
 </div>
<script src="img/vivus.js"></script>
<script>
 new Vivus('loader', {duration: 3000});
</script>
</body>

</html>

Vivus 还有其他配置选项,您可以在这里找到它们:github.com/maxwellito/vivus#option-list。我们不会全部讨论,但我们将说明另一个非常有用的选项,即在动画完成后运行回调函数。

除了我们定义一个简单的回调函数callback,它遍历所有具有类stroke的元素的实例并将它们的描边更改为不同的颜色,其他都与之前的 Vivus 示例相同。

最终结果看起来像以下的截图。一旦动画完成并且回调函数执行,文本将变为红色:

回调函数作为可选的第三个参数传递给 Vivus 构造函数。然后在动画完成时执行。

以下代码示例展示了它是如何工作的:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with Vivus</title>
  <style>
  .stroke{
    stroke-linejoin: round;
  }
  </style>
</head>

<body>
  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
          <svg id="loader"  
            viewBox="0 0 250.23 131.83"><title>Logo</title><path
             fill="none" stroke="#000" 
             d="M160.9,26.9l-.37.25c6.81,8.24,10.62,17.49"/>
<path fill="none" stroke="#000" d="M28.14,92.59c1.43,1.56,2.81,3,4,4.45,3.56,4.31,6.05"/>
<path fill="none" stroke="#000" d="M80.3,57.58c.27,4.74.54,9.34.81,14l-19.33,1v8.1a4.56,4.56,"/>
<path fill="none" stroke="#000" d="M160.9,26.9a5.89,5.89,0,0,1,1.08.74c11.41,"/>
<path fill="none" stroke="#000" d="M28.14,92.59c-3.72,5.21-7.28,"/>
<path fill="none" stroke="#000" d="M80.3,57.58,59.18,59.36V56.54h21C79.42,"/>
<path fill="none" stroke="#000" d="M43.87,73.26a5.31,5.31,0,0,1-.24,5.8c-1.51-.76-1.58-.91-1-2.4Z"/><path fill="none" stroke="#000" d="M103.13,55.28,90"/></svg>
      </div>
    </div>
  </div>
<script src="img/vivus.js"></script>
<script>
  function callback(){
    for (const element of document.getElementsByClassName("stroke")){
      element.style.stroke = "#cc0033";
    };
  }
  new Vivus('loader', {duration: 500}, callback);
</script>
</body>

</html>

现在我们已经看了一个只做一件事的动画库,让我们来看看一个更全面的动画库GreenSock Animation Platform (GSAP)

使用 GSAP 对 SVG 进行动画

GSAP 是用于 Web 动画的一组强大的 JavaScript 工具。它与 SVG 非常配合。

GSAP 是一组强大的工具,深入探索它所提供的所有内容需要跨越多个章节。这还只是免费版本。还有一个高级版本,其中包括更多功能和功能。

好消息是,尽管它非常强大,但 GSAP API 很简单,所以一旦找到所需的功能并查看了强大的文档(greensock.com/docs),您将能够非常快速地做很多事情。

让我们看两个单独的示例,为您介绍 GSAP 可以做什么以及它是如何做到的。

这个第一个示例复制了我们在本章中已经做过几次的相同动画。我们将一个球从 SVG 元素的一边移动到另一边。这个实际上使用了最初 JavaScript 示例中的一些熟悉代码来计算最终位置。

标记与我们迄今为止看到的一样。它是一个 SVG 元素中的一个circle元素,带有circleid

要开始使用 GSAP,我们需要在演示中包含他们的 JavaScript。在这种情况下,我们包含了 TweenMax 脚本。在项目文件夹中运行npm install将安装 GSAP,然后我们可以从项目的node_modules文件夹中包含它。

GSAP 提供了两个不同的 Tween*模块:TweenLiteTweenMax

它们的描述如下:

TweenLite 是一个非常快速,轻量级和灵活的动画工具,它是 GSAP 的基础。TweenLite 实例处理任何对象(或对象数组)的一个或多个属性随时间的变化。

TweenMax 扩展了 TweenLite,添加了许多有用(但非必要)的功能,如 repeat(),repeatDelay(),yoyo()等。它还默认包含许多额外的插件,使其功能非常齐全。

我们将在此演示中使用 TweenMax。如果您要开始尝试 GSAP,TweenMax 将为您提供最大的工具集。它稍微慢一些,但更强大,而且在您尝试使用它时,拥有一切都会更有趣。

现在我们已经加载了 JavaScript 文件,让我们开始使用它。

JavaScript 应该看起来很熟悉,至少起初是这样。我们设置了几个熟悉的常量:doc作为document的别名,canvas作为 SVG 元素的引用,circle作为我们要动画的圆的本地引用,viewBox作为 SVG 元素的viewBox的本地引用,width作为viewBox.widthnewX作为圆元素的计算完成位置。

GSAP 特定的新代码如下,当我们调用TweenMax.to时。TweenMax.to是一个方法,用于将 HTML 元素动画到特定状态。参数如下:

  • "#circle"是用于匹配我们要动画的元素的 CSS 选择器。

  • 1是动画将运行的次数。

  • 最后,有一个配置对象来定义动画。在我们的示例中,我们将newX变量作为cx元素的新值传入。

就是这样;GSAP 处理剩下的部分,平滑地将圆圈从屏幕的一端移动到另一端:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with GSAP</title>

</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450" 
        width="1000" height="450" version="1.1" id="canvas" 
        class="canvas">
          <circle cx="75" cy="225" r="50" fill="blue" id="circle">
        </circle>
        </svg>
      </div>
    </div>
  </div>

<script src="img/TweenMax.min.js"></script>
<script>
const doc = document;
const canvas = doc.getElementById("canvas");
const circle = doc.getElementById('circle');
const viewBox = canvas.viewBox.baseVal;
const width = viewBox.width;
const newX = width - (circle.r.baseVal.value * 3);
TweenMax.to("#circle", 1, {attr:{cx:newX}, ease:Linear.easeNone});
</script>
</body>

</html>

下一个示例具有相同的设置,但更改了传递到TweenMax.to的参数,并添加了另一个链接方法调用以更改动画的持续时间。在这个示例中,我们传入四个单独的属性来对元素进行动画处理,cxcyrfill。这个示例展示了 GSAP 的真正力量之一。您不必弄清楚关于这些多个属性动画的时间,各个间隔的样子,或者如何同步它们并解析它们以使它们平稳运行。您只需给 GSAP 一个最终状态,然后观察它的魔力。

此外,我们正在添加一个新的方法,链接到对TweenMax.to的调用的末尾。调用TweenMax.duration会改变动画的持续时间。在这里,我们传入5,以延长动画的持续时间为整整五秒。这种链接的接口允许您以类似于使用 jQuery 和许多其他 JavaScript 库的方式处理动画。这是一个强大而友好的接口。

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with GSAP</title>

</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450" 
         width="1000" height="450" version="1.1" id="canvas" 
         class="canvas">
          <circle cx="75" cy="225" r="50" fill="blue" id="circle">
        </circle>
        </svg>
      </div>
    </div>
  </div>

<script src="img/TweenMax.min.js"></script>
<script>
const doc = document;
const canvas = doc.getElementById("canvas");
const circle = doc.getElementById('circle');
const viewBox = canvas.viewBox.baseVal;
const width = viewBox.width;
const height= viewBox.height;
TweenMax.to("#circle", 1, {attr:{cx:width,cy:0,r:height,fill:"red"}, ease:Linear.easeNone}).duration(5);
</script>
</body>

</html>

在浏览器中运行上面的代码会产生以下输出:

摘要

在本章中,您了解了关于 SVG 的可视化和动画。这包括使用纯 JavaScript、SMIL、CSS 以及两个用于动画的库:GSAP 和 Vivus。

在本章中,我们看到了:

  • 使用 JavaScript、SVG 和 CSS 创建自定义数据可视化。您使用 JavaScript 处理了一组数据,并使用结果创建了一个漂亮的可视化,以便以易于阅读的方式说明一组数据。

  • 使用 JavaScript 创建自定义 SVG 动画。这包括计算增量以在每秒 60 帧的速度下进行动画,并使用requestAnimationFrame作为一种方法,以确保您提供最流畅的体验。

  • 使用 CSS 对 SVG 进行动画。您了解到,用于对 SVG 进行动画的强大选项具有不确定的浏览器支持。

  • 使用 SMIL 对 SVG 进行动画,这也带来了不确定的浏览器支持。

  • 使用 Vivus 库对 SVG 进行动画,这使得在 SVG 中实现“绘图”动画就像包含库并添加一行 JavaScript 代码一样简单。

  • 最后,您对强大的 GSAP 库有了一瞥,它为 SVG 和其他元素的动画提供了非常强大的选项。

现在我们已经将一些库引入到混合中,将顺利过渡到一个关于 SVG、Snap.svg 和 SVG.js 的辅助库的整个章节。这些是重要的工具,如果您想要在 SVG 上进行高级的自定义工作,它们将是非常宝贵的。

第九章:辅助库 Snap.svg 和 SVG.js

到目前为止,我们在本书中已经学到了很多关于 SVG 的知识。如果你已经走到了这一步,你已经准备好进行一些严肃的 SVG 开发,对此有三种方法:

  • 继续做我们在本书中大部分已经做过的事情-了解核心技术如何相互作用并将 SVG 集成到您的站点或应用程序中,就像您在任何标记中一样。用 JavaScript 和 CSS 操纵它,你就可以准备好处理基本上任何事情。这是一个有效的方法,也是我在自己的工作中经常采用的方法。

  • 使用特定任务的框架和库。我们已经开始用 GSAP 和 Vivus 进行动画的一点探索。我们将在第十章中继续探讨这个问题,使用 D3.js,当我们研究 D3,一个强大的可视化框架。

  • 使用通用的 SVG 库,它将帮助您处理各种与 SVG 相关的任务。SVG 是在一个名为 Raphael 的库的支持下进入了 Web 开发的主流,目前有一些库可供您在自己的工作中使用。这个选项是本章的重点。

如前所述,由于浏览器的有限支持,SVG 花了很多年时间才获得了广泛的应用。一个名为 Raphael.js 的通用 SVG 库通过为较旧版本的 Internet Explorer 提供了一个非常聪明的矢量标记语言(VML)的 polyfill 来弥合了这种支持差距。它还为处理浏览器中的 SVG 提供了一个友好的 API,这有助于那些对 SVG 不熟悉的人快速、轻松地入门。

本章涉及两个最受欢迎的 Raphael.js 的后继者:

  • Snap.svg:是 Raphael 的直接继承者,由 Raphael.js 的作者 Dmitry Baranovskiy(snapsvg.io/)编写的库。

  • svg.js:另一个小巧、轻量级的库,提供了许多强大的选项来操纵 SVG(svgjs.com/)

本章的其余部分将介绍每个库的基础知识,然后通过一些熟悉的例子,重新利用这些通用的 SVG 工具的功能。

我们将从 Snap.svg 开始。

使用 Snap.svg

Snap.svg 是 Adobe 的 SVG 实用库,由 Dmitry Baranovskiy 编写。它功能相对齐全,具有友好、易于探索的 API,并且是开源的。最近这个库的开发速度有所放缓,但它仍然是一个有用的工具,如果您正在探索通用的 SVG 库,您应该意识到它。

让我们开始吧。

开始使用 Snap.svg

Snap.svg 可以在npm上获得,因此最简单的方法是使用npm安装它:

npm install snapsvg

它也可以直接从网站snapsvg.io/下载,也可以从 GitHubgithub.com/adobe-webplatform/Snap.svg下载或克隆。

一旦你做到了这一点,只需包含node_modules中的snap.svg-min.js,或者从下载的文件夹中,你就可以开始使用 Snap 了。

在这个第一个例子中,我们将 Snap 加载到文档中,然后通过一些 Snap 基础知识加载 Snap API 并操纵一些 SVG。

最初,在这个第一个例子中,我们获取了一个包含div的引用,使用 ID#target。然后我们使用new关键字创建了一个 Snap 的实例,并将其存储在变量S中。传入了两个参数,800600。这代表了 SVG 元素的宽度和高度。

在本章中,我们将使用变量S来表示Snap.svg的 API,你可以将变量命名为任何你喜欢的名称,只要你将Snap.svg构造函数的返回值分配给它。S 并没有什么神奇之处,除了它是 Snap 作者在他们的示例中使用的传统变量名。

接下来,我们使用 Snap 的实用方法S.appendTo将我们的新 SVG 元素添加到文档中,使用我们的#target元素作为容器。

现在 SVG 元素已经在页面上,我们向文档中添加了两个新的 SVG 元素,以展示使用 Snap 添加和操作 SVG 元素的基本模式。我们添加了一个圆和一个矩形。圆是用S.circle添加的,传入三个属性,中心 x中心 y半径。一旦圆被添加,我们调用链式方法attr,传入fillstroke

接下来我们调用S.rect来创建一个矩形,传入xywidthheight参数,并再次使用attr来添加fillstroke

类似于 jQuery 的方法链式调用来操作 SVG 元素是与 Snap 交互的核心。如果你有这种开发风格的经验,你会很快掌握 Snap。API 清晰而逻辑,因此很容易进行实验:

<!doctype html>
<html lang="en">

<head>
 <meta charset="utf-8">
 <title>Mastering SVG- Basic Snap.svg demo</title>
 <link rel="stylesheet" 
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.m
  in.css" integrity="sha384-
  Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" 
  crossorigin="anonymous">
</head>

<body>

 <div class="container-fluid">
 <div class="row">
 <div class="col-12" id="target">

 </div>
 </div>
 </div>

 <script src="img/snap.svg-min.js"></script>
 <script>
 const target = document.getElementById("target");
 const S = new Snap(800,600);
 S.appendTo(target);
 S.circle(250,250,100)
 .attr({
 "fill":"blue",
 "stroke":"green"
 });
 S.rect(550,250,100,100)
 .attr({
 "fill":"green",
 "stroke":"blue"
 });
 </script>
</body>

</html>

在浏览器中运行上述代码会产生以下输出:

有了这种基本模式,我们现在可以开始使用 Snap 重新创建之前做过的一些演示。从核心技术过渡到库可以很有启发性,可以让你对库有所了解,以及它是否适合你使用。

使用 Snap 进行动画

由于 SVG 动画是现代网页的一个重要特性,Snap 提供了几个动画工具。它还提供了操作现有 SVG 元素的能力,而不仅仅是 Snap 本身生成的元素(这是 SVG.js 无法做到的)。这个演示利用了这两个功能。

设置与我们之前在这个动画演示的例子中看到的类似。我们通过获取三个元素引用doc(文档)、canvas(父 SVG)和circle(圆圈元素)来开始演示。接下来,我们获取viewBox的引用和相关的width,以便对圆的结束点进行一些计算。这个新的结束点被存储为newX

接下来是这个例子的 Snap 特定特性。首先,我们使用 Snap 的 API 加载了一个对circle元素的引用。我们通过将变量circle,一个对circle元素的 DOM 引用,传递给 Snap 来实现这一点。如果你经常使用 jQuery,这可能对你来说是一个熟悉的模式。

完成后,我们可以使用 Snap 的animate方法来使圆圈在屏幕上移动。在这种情况下,animate接受四个参数:

  1. 第一个是一个对象,指示动画的结束状态。在这种情况下,我们正在将cx属性动画到计算出的newX值。

  2. 然后我们传入动画的持续时间,三秒的毫秒数。

  3. 之后我们传入动画缓动。我们再次使用了弹跳缓动。这是作为 Snap 的mina对象的一部分提供的,它提供了内置的缓动选项以及一些其他用于处理动画的实用工具。

  4. 最后,我们传入一个callback函数,在动画完成后运行。这个函数将填充颜色更改为红色:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with Snap.svg</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/boot
   strap.min.css" integrity="sha384-
   Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6J
   Xm"
    crossorigin="anonymous">
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000
         450" width="1000" height="450" version="1.1" id="canvas"
         class="canvas">
          <circle cx="75" cy="225" r="50" fill="blue" 
           id="circle"></circle>
        </svg>
      </div>
    </div>
  </div>

  <script src="img/snap.svg-min.js"></script>
  <script>
    const doc = document;
    const canvas = doc.getElementById("canvas");
    const circle = doc.getElementById("circle");
    const viewBox = canvas.viewBox.baseVal;
    const width = viewBox.width;
    const newX = width - (circle.r.baseVal.value * 3);
    const S = new Snap(circle);

    S.animate({ "cx": newX }, 3000, mina.bounce, () => {
      S.attr({ "fill": "red" })
    });
  </script>
</body>

</html>

除了在这个例子中看到的动画工具之外,Snap 还包括其他用于处理 SVG 的工具。下一节将介绍其中一些工具。

Snap.svg 工具

这个例子将说明一些可用于处理 SVG 的有用的 Snap 实用程序。使用通用库如 Snap 的目的是使用诸如以下的实用方法。这个例子只显示了两个这样的实用程序,但这应该足以向您展示可用的东西的类型。

示例的开始是标准的Snap.svg开发。您首先获取对#target元素的引用。我们创建一个Snap变量S,然后将其附加到#target元素上。

一旦它在文档中,我们可以使用两个实用程序中的第一个。这是一个单行赋值给变量bbox,它返回 SVG 元素的边界框,这种情况下是一个圆。

边界框是可以包含形状(或一组形状)的最小可能矩形。

让我们看看这个赋值发生了什么。首先,我们在(255255)处创建一个新的circle,半径为110像素。然后我们添加fillstroke,以便在 SVG 元素上看到它。然后我们调用getBbox方法,存储为bbox

当我们console.logbbox变量时,我们看到以下值:

正如您所看到的,返回值包含的信息远不止可以包含元素的最小可能矩形的简单坐标。它包含了这些信息(xyheightwidth),但它还有其他几个属性,如果您正在处理元素与动画、可视化或动态绘图中的另一个元素的关系,这些属性可能会很有用。

以下列表显示了边界框的值及其代表的含义:

  • cx - 盒子中心的x

  • cy - 盒子中心的y

  • h - 盒子的高度

  • height - 盒子的高度

  • path - 盒子的路径命令

  • r0 - 完全包围盒子的圆的半径

  • r1 - 可以包含在盒子内的最小圆的半径

  • r2 - 可以包含的最大圆的半径

  • vb - 作为viewBox命令的盒子

  • w - 盒子的宽度

  • width - 盒子的宽度

  • x2 - 盒子右侧的x

  • x - 盒子左侧的x

  • y2 - 盒子底边的y

  • y - 盒子顶边的y

这是一个非常有用但可能普通的实用方法。正如你将在 SVG.js 部分看到的,边界框是在使用 SVG 时一个重要且常见的概念。

下一个实用程序示例更有趣一些。让我们看看它是如何工作的。

为此,我们首先创建一个代表风格化字母 R 的path。您之前在我们的动画示例中看到了这个 R 和相关的path。一旦字母 R 被插入文档中,我们为其添加fillstroke,然后对其进行变换,以便将其居中放置在我们之前创建的circle上。最终结果如下截图所示:

一旦路径被插入,我们再次调用console.log,使用另一个实用方法path.getTotalLength()作为参数传入。path.getTotalLength()就像它的名字一样 - 它返回引用路径元素的总长度。

例如,如果您正在沿着路径在一定时间长度内进行动画,获取路径的长度将是一个重要的度量。正如下面的截图所示,这个实用程序提供了这个强大的度量,而几乎没有麻烦:

刚刚描述的整个代码如下:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- Snap.svg utilities</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>

  <script src="img/snap.svg-min.js"></script>
  <script>
    const target = document.getElementById("target");
    const S = new Snap(800,600);
    S.appendTo(target);
    const bbox = S.circle(255,255,110)
                    .attr({
                        "fill":"blue",
                        "stroke":"green"
                    }).getBBox();
    console.log("bounding box",bbox);

   const path = 
   S.path("M28.14,92.59c1.43,1.56,2.81,3,4,4.45,3.56,4.31,6.05,9.14,6.3
    9,14.82.37,6.35-2,11.81-5.82,16.7-.61.76-1.27,1.48-
    2,2.35,3.15-.86,6.09-1.74,9.07-2.48,2.82-.7,5.66-1.4,8.54-
    1.82a6.54,6.54,0,0,0,2.84-1.15c4.26-2.9,8.5-5.84,12.87-
    8.56a30.61,30.61,0,0,1,10.12-
    4.23c3.16-.64,6.11-.57,7.81,3a73.85,73.85,0,0,0-.4-7.64c-.51-4.55-
    1.4-9-3.7-13-2.84-5-7-6.39-12.32-4.22a32.44,32.44,0,0,0-
    9.07,6.17c-.38.34-.77.65-1.51,1.26-.88-4.66-1.72-9-5.08-12.1.76-
    1.26,1.5-2.32,2.05-3.46a22.71,22.71,0,0,0,1.38-
    3.57,31.72,31.72,0,0,0,0-16.47c-1-4.39-2.26-8.73-3.33-13.11-.37-
    1.53-.53-3.12-.77-4.58-12-.08-23.06-3.78-34.44-
   6.66L6.21,65.08l14.68,9.47L.83,105.88c5.07.89,9.91,1.7,14.74,2.6a1.5
  ,1.5,0,0,0,1.76-.72C20.86,102.76,24.42,97.8,28.14,92.59Z")
    .attr({"fill":"gray","stroke":"burgundy"})
    .transform("s2 t110,85");

    console.log("total length", path.getTotalLength());

  </script>
</body>

</html>

现在我们已经看了一些 Snap 实用程序,让我们来看看 Snap 的事件系统,它允许您以交互方式使用 SVG 元素,同时仍然紧密地遵循 Snap API 的限制。

Snap.svg 事件

虽然您可能已经掌握了使用Element.addEventListener手动管理事件,或者已经使用类似 jQuery 的东西来处理事件,但值得注意的是,Snap 提供了一些自己的事件工具。这使您可以减少外部依赖,如果您正在专注于 SVG 的工作。它还允许您跳过像 jQuery 这样的库在处理 SVG 元素时提供的任何怪癖。

以下示例是一个熟悉的示例,修改后显示了 Snap.svg 事件的工作原理。在这个示例中,我们再次向空白的 SVG 画布添加click事件处理程序,并在点击点将随机大小的圆插入 SVG 元素。使用 Snap 来实现这个演示与您之前看到的非常相似,但它有一些值得注意的便利,并且说明了 Snap 处理事件的简单方式。

该示例首先获取#target元素的访问权限,设置heightwidth变量,然后创建一个附加到#target元素并存储在标准 Snap 变量S中的 Snap 实例。

一旦我们加载了 Snap,我们将一系列方法调用链接在一起,使用S.circle方法添加一个圆,使用attr方法设置fill,然后使用 Snap 的click事件工具为元素添加点击事件处理程序。

当用户单击 SVG 元素时调用的callback函数与普通 JS 版本几乎相同,尽管它使用 Snap 方法S.circle插入一个圆元素,使用熟悉的随机参数fillradiusnewXnewY

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Events with Snap.svg</title>

</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>

  <script src="img/snap.svg-min.js"></script>
  <script>
    const target = document.getElementById("target");
    const height = 600;
    const width = 800;
    const S = new Snap(width,height);
    S.appendTo(target);
    S.circle(250,250,100).attr({"fill":"blue"}).click(()=>{
      const newX = Math.random() * width;
      const newY = Math.random() * height;
      const r = Math.random() * height/2;
      const red = Math.random() * 255;
      const blue = Math.random() * 255;
      const green = Math.random() * 255;
      S.circle(newX,newY,r).attr({
        "fill":`rgba(${red},${blue},${green},${Math.random()})`
      });
    });

  </script>
</body>

</html>

如果您习惯于使用 jQuery 或其他遵循类似模式的库,那么您应该能够快速掌握 Snap 的事件工具。

使用 Snap.svg 进行自定义数据可视化

最后一个使用Snap.svg的示例显示了它如何用于进行自定义数据可视化。这将展示Snap.svg的许多功能,并提供对该库的最终全面了解。

这个例子将再次生成一个可视化,显示大卫·奥尔蒂兹在波士顿红袜队职业生涯中每年击出的全垒打的正负增量与每年击出的平均全垒打数之间的对比。

由于我们已经看到了这个可视化,在本节中我们将只关注使用Snap.svg的地方,而不是脚本的每一行。如果您需要对数据可视化本身的方法和原因以及如何计算指标进行复习,请回顾第八章,“SVG 动画和可视化”,以获得整个脚本的完整解释。

您将看到的第一个文件是 HTML 文件,它与此可视化的原始版本类似。唯一的真正区别是包括从node_modules中的Snap.svg源文件:

  <div class="container-fluid">
    <div class="row">
      <div class="col-12">
        <svg  viewBox="0 0 1000 450" 
         width="1000" height="450" version="1.1" id="canvas" 
         class="canvas">
        </svg>
      </div>
    </div>
  </div>

  <script src="img/snap.svg-min.js"></script>
  <script src="img/scripts.js"></script>

查看scripts.js的源代码,viz()函数在结构上是相同的,但有一些与 Snap 相关的差异,您会想要注意到。

data变量完全相同,并在此处截断,以使viz()函数稍微易于阅读。请参阅第八章,“SVG 动画和可视化”,或查看源代码以查看完整的数据集。

data变量之后,一些有趣的东西从S变量开始。正如您之前看到的,SSnap.svg的一个实例,这将是我们进行大部分工作的接口。在那之后,在这个版本和原始版本之间没有任何变化,直到我们使用对 SVG 元素的 DOM 节点的 Snap 引用S.node来访问 SVG 元素的viewBox

接下来,你会注意到的最大的区别是能够使用 Snap 的便利方法S.rectS.lineS.text(都与S.attr配对)将我们的线条、方框和文本元素添加到屏幕上。我们还使用S.addClass将 CSS 类添加到我们的线条中。

因为所有这些方法都存在于Snap.svg中,这个例子和我们仅使用 JavaScript 的例子之间最大的区别是我们自己手动编写的便利方法的缺失。由于 Snap 提供了许多便利功能,我们不需要自己提供。这本身就很棒,当然,Snap 包括的便利方法远远多于S.rectS.lineS.textS.attr

function viz() {
  /*
    ES6
  */
  const data = [
    /* truncated for brevity - see Chapter 8 for the full data set*/   
    {
      "year": 2016,
      "hrs": 38
    }
  ];

  const doc = document;
  const canvas = doc.getElementById("canvas");
  const S = new Snap(canvas);
  function maxDiffer(arr) {
    let maxDiff = arr[1] - arr[0];
    for (let i = 0; i < arr.length; i++) {
      for (let j = i + 1; j < arr.length; j++) {
        if (arr[j] - arr[i] > maxDiff) {
          maxDiff = arr[j] - arr[i];
        }
      }
    }
    return maxDiff;
  }
  document.addEventListener("DOMContentLoaded", () => {
    const viewBox = S.node.viewBox.baseVal;
    const width = viewBox.width;
    const height = viewBox.height;
    const x = viewBox.x;
    const y = viewBox.y;
    const padding = width / 200;
    const vizWidth = width - padding;
    const years = data.length;
    const total = data.reduce((total, item) => {
      return total + item.hrs;
    }, 0);
    const avg = total / years;
    const verticalMidPoint = (y + height) / 2;
    const diffs = data.map((item) => {
      return item.hrs - avg;
    });
    const maxDiff = maxDiffer(diffs);
    const yIntervals = verticalMidPoint / maxDiff;
    const xInterval = (vizWidth / years);
    for (const i in diffs) {
      const newX = xInterval * i;
      const newY = diffs[i] * yIntervals;
      if (diffs[i] < 0) {
        S.rect(
          newX + padding,
          verticalMidPoint,
          xInterval - padding,
          Math.abs(newY)
        ).attr({ 
          "fill": "#C8102E", 
          "stroke": "#ffffff" 
        });

        S.text(
          newX + padding, 
          verticalMidPoint + Math.abs(newY) + (padding * 3), 
          `${data[i].hrs} in ${data[i].year}`
        );
      }
      else if (diffs[i] > 0) {
        S.rect(
          newX + padding,
          verticalMidPoint - newY,
          xInterval - padding,
          newY,
        ).attr({ 
          "fill": "#4A777A", 
          "stroke": "#ffffff" 
        });

        S.text(
          newX + padding,
          verticalMidPoint - newY - (padding * 2)
          , `${data[i].hrs} in ${data[i].year}`
        );
      }
      S.line(
        x,
        verticalMidPoint,
        width,
        verticalMidPoint
      ).attr({ 
        "stroke": "#ffffff" 
      });
      S.text(
        x + padding,
        height - (padding * 3)
        `Based on an average of ${avg} home runs over ${years} years`
       ).addClass("large");
    }
  });

}

viz();

现在我们已经仔细研究了Snap.svg,并希望让你感受到与它一起工作的感觉,让我们再看看另一个helper库,名为 SVG.js。

使用 SVG.js

SVG.js 是由 Wout Fierens 创建的,目前由 Ulrich-Matthias Schäfer、Jon Ronnenberg 和 Rémi Tétreault 维护。它被设计成轻量级和快速,并且是一个友好的 SVG 工作界面。它的维护活跃度比Snap.svg更高,所以它有这个优势。在撰写本文时,最近的代码是在过去两周内添加到项目中的。

开始使用 SVG.js

Snap.svg一样,SVG.js 也可以在npm上获得,因此使用npm安装 SVG.js 是最简单的方法:

npm install svg.js

确保你使用npm安装svg.js而不是svg.js。两者都可以使用并且都指向正确的项目。然而,svg.js已经过时,因为官方包是svg.js

它也可以直接从svgjs.com/installation/#download下载。也可以从 GitHub 的svgjs.com/下载或克隆,并且可以在cdnjs.上找到。

一旦你做到了,只需包含node_modules或下载文件夹中的svg.min.js,你就可以开始使用 SVG.js 了。

这个第一个例子重复了之前的蓝色圆/绿色方块演示。SVG.js 的约定,如他们的演示所示,是使用一个变量draw来保存你要使用的 SVG.js 的加载实例。

要创建 SVG.js 的实例,你需要传入一个目标 HTML 元素的引用,SVG.js 会将一个加载好的 SVG 元素插入到目标元素中,准备让你使用。然后你可以链式调用SVG.size方法,它会设置新创建的 SVG 元素的大小。

在本章中,我们将使用变量draw来表示 SVG.js API,你可以用任何你喜欢的变量名。只要将 SVG.js 构造函数的返回值分配给它,任何变量名都可以使用。draw并没有什么特别神奇的地方,除了它是 SVG.js 作者在他们的示例中使用的传统变量名。

Snap.svg和变量S也是如此。这些只是约定。

SVG.js 并不是为了与现有的 SVG 元素一起工作而设计的,因此如果你习惯于获取现有 SVG 元素的引用然后对其进行操作,你必须稍微改变你的方法。

一旦我们有了对draw的引用并且我们的 SVG 元素添加到页面上,我们就可以开始操纵 SVG 元素,添加我们的正方形和圆形。

看看圆的例子,我们调用了名为draw.circle的方法来创建一个圆。draw.circle接受一个参数,即圆的半径

有趣的是,所有其他属性都是用熟悉的(来自 jQuery 和 Snap 的)attr方法进行操作。我认为这是一个奇怪的选择,因为只有半径的圆并不是很有用。对于draw.rect也是一样,它需要矩形的高度和宽度作为参数,然后使用attr作为其他属性。

这种语法完全有效。但有趣的是属性分布在两个方法中:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- Basic SVG.js demo</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>

  <script src="img/svg.min.js"></script>
  <script>
    const draw = SVG('target').size(800,600);
    draw.circle(200)
        .attr({
          "fill":"blue",
          "stroke":"green", 
          "x":250,
          "y":250
        });
    draw.rect(100,100)
        .attr({
          "fill":"green",
          "stroke":"blue", 
          "x":550,
          "y":250
        });
  </script>
</body>

</html>

SVG.js 动画

现在我们已经看到了将元素插入页面的基本示例,让我们继续遵循与Snap.svg相同的模式,并看看如何使用 SVG.js 创建动画。

我们需要另一个依赖项才能在 SVG.js 中正确运行动画,svg.easing.js。这是一个与 SVG 动画一起使用的缓动函数库:

npm install svg.easing.js

在包含主 SVG.js 文件之后包含它,然后您就可以开始了。

开始使用这个例子,我们创建了几个变量来在整个动画中使用,widthheightcxcyradius。您之前看到过这些,它们映射到 SVG 元素的属性。

然后我们创建了我们的 SVG.js 实例,使用heightwidth值作为参数,并将其存储在draw变量中。之后我们通过调用draw.circle创建了我们将要进行动画的circle元素,参数是radius变量。然后我们调用attr,传入蓝色的fill值和cxcy变量作为cxcy属性的值。这在 SVG 元素上正确的位置创建了蓝色的圆。

然后我们计算了newX变量。然后我们使用 SVG.js 方法circle.animate将圆形动画到新值。animate方法接受三个参数,3000,动画的长度,SVG.easing.bounce,要使用的缓动函数(来自svg.easing.js),和1000,动画延迟。

接下来是一个链式操作方法,center,在这个例子中,表示要执行的动画类型。center本身将元素的中心移动到传入的新(x,y)坐标。将其与animate链接意味着您将在两个状态之间平滑地进行动画。在我们的例子中,centernewX和原始cy变量作为参数,这为我们提供了新的水平放置位置,同时保留了原始的垂直放置位置。

最后,为了说明动画callback方法,我们使用after方法,它允许我们在动画完成后运行一个函数。在这里,我们只是使用attr方法改变了圆的颜色:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG Animation with SVG.js</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="canvas">

      </div>
    </div>
  </div>

  <script src="img/svg.min.js"></script>
  <script src="img/svg.easing.min.js"></script>
  <script>
    const width = 1000;
    const height = 450;
    const radius = 50;
    const cx = 75;
    const cy = 225;
    const draw = SVG('canvas').size(width,height);
    const circle = draw.circle(radius * 2)
                        .attr({
                            "fill":"blue",
                            "cx":cx,
                            "cy":cy
                         });
    const newX = width - (radius * 3);
    circle.animate(3000, SVG.easing.bounce, 1000)
      .center(newX,cy)
      .after(function(situation) {
        this.attr({ 
          "fill": 'red' 
        });
      });

  </script>
</body>
</html>

正如我们在这两个示例中看到的,SVG.js API 中有一些怪癖。由于这些怪癖是一致的,比如在两个链接的方法中设置属性,您可以非常快速地适应它们。

SVG.js 实用程序

Snap.svg一样,SVG.js 有一套实用函数,可以帮助您处理 SVG。其中一些确实很棒。这个例子展示了其中许多函数的工作原理。

为了开始这个例子,我们创建了一个加载了 SVG.js 变量draw,并传入800600作为heightwidth

立即开始使用一些实用程序,我们调用draw.viewbox()来获取 SVG 元素的viewBox。如果您还记得使用Snap.svg完成的可视化示例,您会记得我们必须导航多个属性才能访问Snap中的viewBox。根本没有方便的方法,只是表示 SVG 元素的 DOM 节点的属性。

这里有一个方便的方法直接返回它:

接下来,我们使用rect加载一个100100的矩形,位于(100, 100),然后console.logrect.bbox(),它返回矩形的边界框。正如您在下面的截图中所看到的,它的属性比Snap.svg示例的边界框要少,但它仍然具有所有您需要与该元素进行干净交互的标准属性:

下一个非常有用的与标准边界框相关的实用程序被说明了。

首先,我们使用 SVG.js 的transform方法转换矩形,将其旋转 125 度。transform是一个getter/setter,当没有参数调用时,将返回当前的转换值,当使用参数调用时,将设置该值。

一旦我们转换了rect矩形,我们就会console.logrect.rbox()的返回值,它返回一个表示元素的可视表示的边界框,其中包括所有的变换。*如果你正在处理变换后的元素,这将节省你大量的编码工作:

接下来的方法data的工作方式与 jQuery 的 data 方法完全相同。作为setter调用时,rect.data({"data":"storing arbitrary data"}),data在对象上设置任意数据,存储在用户提供的标签下。作为getter调用时,传入标签作为参数,rect.data("data"),它返回标记数据的值:

下一个实用方法允许你调整 SVG 元素的堆栈。与绝对定位的 HTML 元素不同,它们具有显式的堆叠顺序(z-index),SVG 元素是基于它们在 DOM 中的出现顺序进行分层的。在 DOM 中后出现的元素似乎位于先出现的元素的顶部。

下一个代码块展示了如何使用 SVG.js 实用程序调整这个堆叠顺序。

首先,我们创建两个正方形,一个绿色的正方形,然后是一个蓝色的正方形。当它们最初出现在屏幕上时,它们看起来如下截图所示:

然后,在一秒的超时内,我们调用back()方法,将元素发送到堆栈的底部。之后,正方形看起来如下:

现在我们在屏幕上有两个正方形,是时候看一下最后一个非常有用的边界框相关实用程序了。如果你调用first.bbox().merge并将second.bbox()作为参数传入,你将得到一个合并的边界框。如果你正在处理不属于结构化 SVG 组的多个元素,这将非常有用:

这是整个代码示例:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG.js utilities</title>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="canvas">

      </div>
    </div>
  </div>

  <script src="img/svg.min.js"></script>
  <script>
    const draw = SVG('canvas').size(800,600);
    console.log("view box:",draw.viewbox());
    const rect = draw.rect(100,100)
                    .attr({
                        "x":100,
                        "y":100
                     });
    console.log("bounding box:", rect.bbox());
    rect.transform({ rotation: 125 });
    console.log("rbox:",rect.rbox());
    rect.data({"data":"storing arbitrary data"});
    console.log("data method:", rect.data("data"));

    const first = draw.rect(50,50)
                      .attr({
                          "x": 200,
                          "y": 200, 
                          "fill": "green"
                       });
    const second = draw.rect(50,50)
                        .attr({
                            "x": 225,
                            "y": 225, 
                            "fill": "blue"
                        });
    setTimeout(()=> {
      second.back();
    },2000);
    console.log("merged bounding box", first.bbox().merge(second.bbox()));

  </script>
</body>

</html>

SVG.js 事件

SVG.js 还具有事件处理工具。下面的示例将说明 SVG.js 提供的非常熟悉的事件处理模式。

我们再次通过将click事件绑定到一个函数来说明事件处理,该函数在画布上插入随机大小的圆和随机填充。这也将说明 SVG.js front()方法的一个很好的用法。

示例从创建draw变量开始,设置其高度和宽度,然后创建一个带有 SVG.js 增强的circle元素的circle变量。

之后,我们将click事件绑定到圆上,使用事件工具circle.click创建随机大小/填充的圆元素。这很简单。就像Snap.svg示例或早期版本的 jQuery 示例一样,你将callback方法作为参数传递给click,这就是正确绑定事件所需的全部内容。

callback中,我们使用draw.circle来创建我们的圆,每次函数运行时都会生成随机值。

在这里使用 SVG.js 的一个好处是,你可以通过在每个圆添加后调用circle.front()来确保可点击的圆始终位于堆栈的顶部。否则,它最终可能会被其他在 DOM 中后插入的元素埋没,因为它们出现在它的上面:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- SVG.js Events

  </title>
</head>

<body>

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>

  <script src="img/svg.min.js"></script>
  <script>
    const height = 600;
    const width = 800;
    const draw = SVG('target').size(width,height);
    const circle = draw.circle(100)
                    .attr({
                      "fill":"blue",
                      "cx":250,
                      "cy":250
                    });

    circle.click((e)=> {
      const newX = Math.random() * width;
      const newY = Math.random() * height;
      const r = Math.random() * height/2;
      const red = Math.random() * 255;
      const blue = Math.random() * 255;
      const green = Math.random() * 255;
      draw.circle(r)
        .attr({
            "cx": newX,
            "cy": newY,
            "fill":`rgba(${red},${blue},${green},${Math.random()})`
          });
      circle.front();
    });

  </script>
</body>

</html>

使用 SVG.js 进行自定义数据可视化

本章的最后一个示例是另一个自定义数据可视化的示例。我们将再次回顾代表大卫·奥尔蒂兹作为波士顿红袜队成员的职业生涯中的全垒打的可视化。

由于我们已经看到了这个多次,我们可以简单地专注于 SVG.js 如何帮助我们完成这项工作。

你将看到的第一个文件是 HTML 文件。与纯 JS 版本之间唯一的区别是包含了来自node_modules的 SVG.js 源文件,以及没有基本 SVG 元素:

  <div class="container-fluid">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>
  <script src="img/svg.min.js"></script>
  <script src="img/scripts.js"></script>
</body>

viz()函数与Snap.svg版本中看到的类似。再次,我们为了可读性对数据对象进行了剪裁。

接下来是使用 SVG.js 的熟悉模式。我们设置widthheight变量,然后使用widthheight变量作为参数创建draw SVG.js 实例。

SVG.js 首次发挥作用的地方是DOMContentLoaded 回调函数中易于使用的viewBox()方法,该方法返回 SVG 元素的viewBox。我们使用这个变量来计算可视化中使用的多个变量。在创建了超过 20 行熟悉变量之后(请参阅第八章,SVG 动画和可视化,以便了解每个变量的作用),我们绘制了一些框,画了一些线,并添加了一些文本。

让我们看一个 SVG.js 如何帮助解决这些问题的例子。

绘制框允许我们暴露一些 SVG.js 提供的便利方法,作为attr中属性设置的替代。draw.rect的调用方式与以前相同,传入每个框的计算宽度和高度。然后,我们对其进行了三次方法调用:attr用于设置xy,然后,作为它们可用性的说明,我们还使用了两个便利方法fillstroke,直接设置了fillstroke。完全可以将所有内容设置为attr的参数,但如果您喜欢以这种方式链接方法调用,那么调用fillstroke来设置这些属性是一个不错的选择。

绘制文本引入了一个新方法draw.plain。有一个draw.text方法,但draw.text设计用于处理更大的文本块,因此引入了tspan元素来帮助控制流和换行。这实际上非常聪明,对于许多情况下需要处理 SVG 中的长文本块的情况来说,这是一个有用的选择,因为一切与流和换行有关的事情都必须手动处理。在这些情况下,有多个元素可供使用是很好的。

然而,draw.plain非常适合我们这里的需求,因为我们只对单个文本元素感兴趣。要使用它,我们调用draw.plain,将我们连接的字符串作为参数传入,然后使用我们的好朋友attr设置(x,y)坐标。

绘制线需要四个初始参数,起始(x,y)和结束(x,y)。一旦我们提供了viz()函数的其余部分计算出的这些值,我们就可以执行诸如添加描边之类的操作,通过draw.attr(就像这个例子中一样)或draw.stroke(如果您喜欢),或者使用便利方法draw.addClass添加类。

function viz() {
  /*
    ES6
  */
  const data = [
/* truncated for brevity - see Chapter 8 for the full data set */
    {
      "year": 2016,
      "hrs": 38
    }
  ];
  const width = 1000;
  const height = 450;
  const draw = SVG("target").size(width, height);
  function maxDiffer(arr) {
    let maxDiff = arr[1] - arr[0];
    for (let i = 0; i < arr.length; i++) {
      for (let j = i + 1; j < arr.length; j++) {
        if (arr[j] - arr[i] > maxDiff) {
          maxDiff = arr[j] - arr[i];
        }
      }
    }
    return maxDiff;
  }
  document.addEventListener("DOMContentLoaded", () => {
    const viewBox = draw.viewbox();
    const width = viewBox.width;
    const height = viewBox.height;
    const x = viewBox.x;
    const y = viewBox.y;
    const padding = width / 200;
    const vizWidth = width - padding;
    const years = data.length;
    const total = data.reduce((total, item) => {
      return total + item.hrs;
    }, 0);
    const avg = total / years;
    const verticalMidPoint = (y + height) / 2;
    const diffs = data.map((item) => {
      return item.hrs - avg;
    });
    const maxDiff = maxDiffer(diffs);
    const yIntervals = verticalMidPoint / maxDiff;
    const xInterval = (vizWidth / years);
    for (const i in diffs) {
      const newX = xInterval * i;
      const newY = diffs[i] * yIntervals;
      if (diffs[i] < 0) {
        draw.rect(
          xInterval - padding,
          Math.abs(newY)
        )
        .attr({
          "x": newX + padding,
          "y": verticalMidPoint,
        })
        .fill("#C8102E")
        .stroke("#ffffff");

        draw.plain(`${data[i].hrs} in ${data[i].year}`)
        .attr({
          "x": newX + padding,
          "y": verticalMidPoint + Math.abs(newY) + (padding * 3)
        });
      }
      else if (diffs[i] > 0) {
        draw.rect(
          xInterval - padding,
          newY,
        )
        .attr({
          "x": newX + padding,
          "y": verticalMidPoint - newY
        })
        .fill("#4A777A")
        .stroke("#ffffff");

        draw.plain(`${data[i].hrs} in ${data[i].year}`)
        .attr({
          "x": newX + padding,
          "y": verticalMidPoint - newY - (padding * 2)
        });
      } 
    }
    draw.line(
      x,
      verticalMidPoint,
      width,
      verticalMidPoint
    )
    .attr({ 
      "stroke": "#ffffff" 
    });

    draw.plain(`Based on an average of ${avg} home runs over ${years} years`)
    .attr({
      "x": x + padding,
      "y": height - (padding * 3)
    })
    .addClass("large");
  });

}

viz();

摘要

本章为您提供了两个用于处理 SVG 的独立库Snap.svg和 SVG.js 的快速介绍。在这两个库中,使用相同的熟悉任务,您可以看到使用原始 JS 和使用库进行这些 SVG 操作之间的区别。您还可以比较两个库在类似任务上的差异。

总的来说,通过这两个库,您学到了许多不同的主题,包括如何入门,如何为元素添加动画,如何处理事件,以及如何进行自定义数据可视化。

现在我们已经了解了通用库,我们将最后看一下一个非常特定目的的 SVG 库,D3.js。D3 用于重型数据可视化,并且是处理 SVG 的最强大的工具之一。

第十章:使用 D3.js

这一章将向您介绍数据驱动文档D3),这是一个功能强大的可视化库,也是世界上最受欢迎的开源项目之一。有趣的是,尽管它最重要的是其数据操作功能,但 D3 只是用于直接处理 SVG 的最强大的库之一。即使在作为我们在上一章中讨论的helper库的上下文中,它也有许多非常有用的功能,用于处理 SVG 文档,包括许多复制Snap.svg和 SVG.js 提供的功能以及更多功能。

然而,D3 并不止于此。它远远超出了 SVG 创作和实用功能集,并提供了丰富的工具套件,用于数据操作和随后生成数据可视化。此外,D3 在底层使用了您在整本书中一直在使用的相同的 Web 标准,并将其与强大的 API 结合在一起,为处理 SVG 和数据提供了一个真正的游乐场。

D3 诞生于一个名为 Protovis 的早期可视化库(mbostock.github.io/protovis/),自 2010 年代初以来一直存在,并且仍由项目的原始开发人员 Mike Bostock 密切关注。该项目正在积极开发,并提供大量文档和丰富的示例供学习。

一旦你掌握了它,它也会很有趣。这是本书介绍的最后一个新技术,用于直接处理 SVG,因此很高兴能以一个高潮结束本书的这一阶段。

让我们玩得开心。

在本章中,我们将学习一些主题,包括:

  • 如何安装 D3 以及如何使用库进行基本的 SVG 操作

  • 如何使用 D3 使用比例尺和帮助定义图表的xy轴来制作条形图

  • 如何使用d3-fetch实用程序获取和解析 JSON 和 CSV 数据

  • 如何使用enterexit选择来根据数据集的更改操作 SVG DOM

  • 如何使用 D3 的arcpie函数实现甜甜圈图表

  • 如何实现和弦图;一个包含多个组件的复杂可视化

开始使用 D3

D3 API 可能需要一些时间来适应。本章的示例将努力说明一些基本概念,并随着我们的深入展示 D3 所提供的一些最佳功能。

在做任何事情之前,您需要将 D3 引入您的页面。为此,您可以使用npm将其安装到您的项目文件夹中:

npm install d3

安装完成后,您可以使用脚本标签从您的文档中链接到压缩的 D3 源代码:

<script src="img/d3.min.js"></script>

如果您不想使用npm,也可以直接从d3js.org链接到它:

<script src="img/d3.v5.js"></script>

此外,如果您想要本地副本,可以从 GitHub(github.com/d3/d3)克隆项目,或者从d3js.org下载项目,然后以任何您喜欢的方式组织您的文件。

安装完成后,您就可以开始探索 D3 API 了。

以下示例显示了如何使用 D3 实现一个简单的条形图。在本书中,您已经看到了用于生成条形图的一些概念,但这里的区别在于 D3 会为您完成。D3 了解所有关于可视化的知识,因此它将为您生成所需的度量标准。

这个可视化将比较有史以来销量最高的十本个人漫画书。它将说明的数据如下:itsalljustcomics.com/all-time-record-comic-book-sales/

标题/期号/等级 销售日期 销售价格
动作漫画 1 9.0 2014/08/24 $3,207,852.00
动作漫画 1 9.0 2011/11/30 $2,161,000.00
动作漫画 1 8.5 2018/06/13 $2,052,000.00
动作漫画 1 8.5 2010/03/29 $1,500,000.00
了不起的幻想 15 9.6 2011/03/09 $1,100,000.00
侦探漫画 27 8.0 2010/02/25 $1,075,000.00
动作漫画 1 堪萨斯城 8.0 2010/02/22 $1,000,000.00
动作漫画 1 5.5 2016/08/04 $956,000.00
全明星漫画 8 9.4 2017/08/27 $936,223.00
动作漫画 1 5.0 2018/03/20 $815,000.00

可视化的最终结果将如下截图所示:

本章中的所有 JavaScript 代码都是为了充分利用 ES6 功能而编写的,比如箭头函数、const 和 let。

接下来是非常简单的标记。我们再次包括 Bootstrap 来进行简单的布局任务和Raleway,这本书中我们选择的字体。然后我们为文本元素设置了一些基本的 CSS 样式,并设置了一个简单的容器来容纳可视化内容。之后,我们包括了三个文件:d3.min.js,主要的 D3 文件,d3-fetch.min.js,D3 的 Fetch 实用程序(developer.mozilla.org/en-US/docs/Web/API/Fetch_API)),以及我们的可视化文件bar.js

<!doctype html>
<html lang="en">

<head>
 <meta charset="utf-8">
 <title>Mastering SVG- D3 Bar Chart</title>
 <link rel="stylesheet" 
   href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.
    min.css" integrity="sha384-
    Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
 <link href="https://fonts.googleapis.com/css?family=Raleway" 
    rel="stylesheet">
 <style type="text/css">
  text {
   font-family: Raleway;
   font-size: 1em;
  }
 </style>
</head>

<body>
 <div class="container">
  <div class="row">
   <div class="col-12" id="target">

   </div>
  </div>
 </div>
 <script src="img/d3.min.js"></script>
 <script src="img/d3-fetch.min.js"></script>
 <script src="img/bar.js"></script>
</body>
</html>

由于这里的 JavaScript 很复杂,并引入了许多新概念,我将逐个解释每个块。如果您想一次看到整个文件,请查看下载的源代码中的完整文件。

查看bar.js,它包含一个在屏幕上绘制整个可视化的函数。函数的开始设置了几个常量,这些常量在整个可视化过程中都被使用:widthheightchartHeight(用于设置图表本身的大小与整个 SVG 的大小)和一个margin常量,用于确保 SVG 元素中有足够的边距来容纳整个可视化内容:

function bar() {
  const width = 960,
    height = 800,
    chartHeight = 600,
    margin = 30;

之后,我们开始直接使用 D3。D3 允许您访问和操作现有的 SVG 元素,并且,就像本书中 D3 演示的情况一样,生成一个经过 D3 增强的 SVG 元素并将其附加到 DOM 中。

在这种情况下,我们使用 D3 的查询选择器实用程序d3.select来选择#target元素,然后将一个新的 SVG 元素附加到其中。然后,我们使用越来越熟悉的命名函数attr来设置 SVG 元素的heightwidth。一旦 SVG 元素在文档中,我们附加一个新的g元素,并立即通过xy轴上的margin进行平移。

链接的 D3 方法的行为类似于 jQuery 或其他使用这种模式的库,因此变量svg是对链中最终元素的 D3 启用引用,即新添加的g。任何与该变量交互的内容都将从该g元素的上下文开始:

 let svg = d3.select("#target").append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g")
    .attr("transform", `translate(${margin},${margin})`);

接下来,我们使用一些方法来设置xy轴的比例,然后实际生成xy轴。这就是 D3 真正发挥作用的地方。做这项工作并不是不可能的。这通常是简单的数学。只是没有人想一直编写这些函数,D3 通过一整套比例函数(github.com/d3/d3-scale)使其变得容易。

x变量保存了scaleBand方法调用的返回值。scaleBand允许您将数值比例划分为组件band,我们将使用它来创建条形图的水平间距。初始调用链接到两个后续调用,每个调用都通知了我们特定可视化的 band。range方法调用将x比例尺设置为从10像素到计算出的上限(width减去两个水平边距)。paddingInner设置 band 的内部填充。这个属性允许我们在列之间创建一些空间。

y变量被创建为线性比例尺。线性比例尺是两个值之间的连续、常规比例尺。这个特定比例尺的值是通过调用range并将chartHeight0作为范围值来设置的。

随后,我们使用新创建的xy比例尺调用了两个便利方法,axisLeftaxisBottom。这些方法为比例尺渲染了可读的参考标记。创建了xAxis,然后将刚刚创建的x比例尺传递给xAxis,以将xAxisx比例尺的值连接起来。y轴的生成方式完全相同:

 let x = d3.scaleBand()
    .range([10, (width - margin.left - margin.right)])
    .paddingInner(0.1);
  let y = d3.scaleLinear()
    .range([chartHeight, 0]);
  let xAxis = d3.axisBottom()
    .scale(x);
  let yAxis = d3.axisLeft()
    .scale(y);

然后,我们使用另一个比例尺方法scaleOrdinal来创建我们的离散数据值和相应一组颜色之间的映射:

 let color = d3.scaleOrdinal()
    .range([
      "#1fb003",
      "#1CA212",
      "#199522",
      "#178732",
      "#147A41",
      "#126C51",
      "#0F5F61",
      "#0C5170",
      "#0A4480",
      "#073690"
    ]);

该方法的其余部分使用了d3-fetchd3.json中的实用程序来访问我们的数据文件,然后作为fetch请求的callback来处理数据并生成我们的可视化。

callback方法以对xy轴的domain进行两次调用开始。

对于序数比例尺,xAxisdomain接受一个数组,并将比例尺的域设置为数组中的特定值集。在这里,我们map返回的data以创建title属性的集合,作为xAxis中使用的值。

对于线性比例尺,调用domain将连续比例尺限制为特定的值集。在这种情况下,我们将比例尺设置为最小值为0,最大值为d3.max的返回值,该返回值为数组中的最大值。

接下来,我们开始操作 SVG 元素来创建实际的可视化效果。

第一组链接的方法附加了一个新的 SVG 组元素g,并向其添加了一对类xaxis,然后将其转换为一个点(0, chartHeight)。这将该组放置在图表底部,这正是您希望x轴的图例所在的位置。

然后我们使用d3.call函数调用xAxis并生成我们的x轴。d3.call是一个实用方法,允许您在选择上调用一个函数,然后返回修改后的选择。这使您能够以一种启用链接的方式将一些功能封装在可重用的函数中。在这里,我们调用xAxis,即我们之前创建的axisBottom方法,以创建x轴 - 包括构成x轴的所有元素。不做其他任何操作,x轴现在看起来像下面这样:

如您所见,对于某些值,该布局可能是可以的,但对于我们的目的来说,它并不实用。由于我们标题的长度,我们需要调整标签以便可读。我们将它们旋转 90 度。

为此,我们在当前链上再链接了一些方法。首先,我们选择了当前选择的所有子节点中的所有text元素。这些都是我们刚刚用xAxis创建的所有text元素。一旦我们有了这个选择,我们就对文本元素应用了-90 度的旋转。这重新定位了文本为垂直。随后,我们调整了dxdy属性,使文本整齐地排列。

接下来,我们附加一个新的g元素。

使用这些组并不是严格必要的,但它们有助于组织生成的代码以进行调试,并且使您更容易创建易于操作的选择。这就是组的作用。

这个新的g元素将保存y轴。y轴的创建方式与x轴类似 - 尽管这是一个更简单的过程,因为不需要操作文本元素。水平文本布局对y轴来说是可以的。在这个调用中,我们向g元素添加了yaxis类,然后调用yAxis,它生成了构成y轴的所有元素。

在这个callback函数中的最终方法链展示了在 D3 中工作时的常见模式。第一个调用是d3.selectAllselectAll将访问与提供的选择器匹配的所有元素。返回的值在 D3 中称为selection。选择可以是 DOM 元素的列表,或者在这种情况下,是与数据中的项目匹配的占位符元素的数组。因此,在这种情况下,空是可以的,因为我们将根据接收到的数据来处理选择并向其添加元素。

我们将在下一节更深入地说明enter和相关方法exit,但简而言之,如果您的选择的元素少于数据集中的点数,则这些额外的数据点将存储在所谓的enter 选择中。调用enter允许我们进入并操作这个进入选择。在我们的情况下,我们正在向 SVG 元素添加许多rect元素。

这些rect元素中的每一个都以以下方式进行操作:

  • fill是参考color比例的成员设置的。

  • x属性是基于x比例的成员创建的。

  • width是使用x-bandwidth计算的,这是一个根据该比例计算宽度的方法,包括任何定义的填充。

  • y属性是基于先前创建的y比例创建的

  • height是通过从chartHeight减去此数据点的y比例值来计算的。这实际上是将框从y值悬挂到图表底部。

所有这些属性组合在一起创建了可视化的核心:

  d3.json("data/top-ten.json").then((data) => {
    x.domain(data.map((d) => {
      return d.title;
    }));
    y.domain([0, d3.max(data,(d) => {
      return d.price;
    })]);
    svg.append("g")
      .attr("class", "x axis")
      .attr("transform", `translate(0, ${chartHeight})`)
      .call(xAxis)
      .selectAll("text")
      .style("text-anchor", "end")
      .attr("transform", "rotate(-90)")
      .attr("dx", -10)
      .attr("dy", -5);
    svg.append("g")
      .attr("class", "y axis")
      .call(yAxis);
    svg.selectAll("rect")
      .data(data)
      .enter().append("rect")
      .style("fill", (d) => {
        return color(d.price);
      })
      .attr("x", (d) => {
        return x(d.title); })
      .attr("width", () => {
        return x.bandwidth();
      })
      .attr("y", (d) => {
        return y(d.price);
      })
      .attr("height", (d) => {
        return chartHeight - y(d.price);
      });
  });
}
bar();

文件的最后一行只是调用bar()来创建可视化。

D3 的 enter 和 exit

正如我在上一节中提到的,我想简要地看一下enter和相关方法exit。这些方法对于处理动态数据集非常重要。使用这些方法,您可以获取任意选择,将其与数据混合,然后使用 D3 的工具对其进行操作,以创建可视化效果。

在这一部分,您将看到三个例子。第一个示例展示了使用enter的示例,说明了对完全空选择调用该方法。第二个示例说明了在具有现有元素的选择上调用enter。第三个示例说明了exit的工作原理。

在这个第一个示例中,我们选择#target元素,然后使用p作为参数调用selectAll。由于#target元素中没有段落,这是一个空选择。在其上调用data将空选择绑定到我们的数据。在绑定的选择上调用enter允许我们根据每个数据点来操作我们的选择。

如果此时记录d3.select("#target").selectAll("p").data(data).enter()的返回值,它将看起来像以下的屏幕截图,显示一个包含原始数据的五个元素的数组,存储为内部的__data__属性:

接下来,我们简单地为每个数据点在文档中append一个段落,并使用text方法将代表数据的文本节点插入文档中:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- D3 Enter</title>
</head>

<body>
  <div class="container">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>
  <script src="img/d3.min.js"></script>
  <script src="img/d3-fetch.min.js"></script>
  <script>
    function enter() {
      const data = ["a", "b", "c", "d", "e"];
      d3.select("#target")
        .selectAll("p")
        .data(data)
        .enter().append("p")
        .text((d) => d);
    }
    enter();

  </script>
</body>

</html>

在浏览器中运行代码会产生以下输出:

下一个示例类似,只是在#target div中有一个现有的段落元素。由于存在p元素,在选择上调用d3.select("#target").selectAll("p").data(data).enter()的结果如下。如您所见,_groups数组具有相同的五个成员,但第一个条目,与selection中的现有成员对应的条目为空。这是因为它不是进入选择的一部分(因为它对应于现有元素):

这个示例的其他内容与使用enter的上一个示例相同:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- D3 Enter with existing content</title>
</head>

<body>
  <div class="container">
    <div class="row">
      <div class="col-12" id="target">
        <p>This is an existing paragraph</p>

      </div>
    </div>
  </div>
  <script src="img/d3.min.js"></script>
  <script src="img/d3-fetch.min.js"></script>
  <script>
    function enter() {
      const data = ["a", "b", "c", "d", "e"];
      d3.select("#target")
        .selectAll("p")
        .data(data)
        .enter()
        .append("p")
        .text((d) => d);
    }
    enter();

  </script>
</body>

</html>

由于在这个示例中只更新了输入选择,因此在浏览器中运行上述代码会产生以下输出:

要更新整个选择,您只需要在更新输入选择之前操纵原始选择:

      const data = ["a", "b", "c", "d", "e"];
      d3.select("#target")
        .selectAll("p")
        .data(data)
        .text((d) => d)
        .enter()
        .append("p")
        .text((d) => d);

exit选择允许您清理不再与数据关联的元素。以下示例显示了这是如何工作的。

render函数最初通过一些我们已经看到的模式。该函数在#target div的子元素p上调用selectAll,加载数据,进入输入选择,并附加一系列带有正确数据的段落元素。

接下来我们重复这个过程,而不是调用enter,我们调用exit,然后立即调用removeexit选择返回选择中不对应数据点的任何元素。remove从文档中删除这些元素。第一次运行时,没有元素被删除,因为数据刚刚被加载。选择中的所有元素都用正确的数据填充。

有趣的事情发生在setTimeout。在那个callback函数中,如果数据数组仍然有成员,就会调用data.pop()pop从数组中删除最后一个元素,然后在 1 秒后递归调用render。当函数再次运行并且我们到达退出选择时,我们调用exit.remove,数据和选择之间存在不匹配。第一次递归调用时,有五个段落,但只有四个数据点。因为第五个段落没有与之关联的数据点,所以它从文档中删除。

这个过程重复,直到没有数据点或段落剩下,递归调用停止:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Mastering SVG- D3 Exit</title>
</head>

<body>
  <div class="container">
    <div class="row">
      <div class="col-12" id="target">

      </div>
    </div>
  </div>
  <script src="img/d3.min.js"></script>
  <script>
      const data = ["a", "b", "c", "d", "e"];
      function render(){
        d3.select("#target")
          .selectAll("p")
          .data(data)
          .enter()
          .append("p")
          .text((d) => d );
        d3.select("#target")
          .selectAll("p")
          .data(data)
          .exit()
          .remove();
        if (data.length) {
          setTimeout(()=>{
            data.pop();
            render();
          }
          ,1000);
        }
      }
    render();
  </script>
</body>

</html>

希望这些简化的例子足以说明这种非常强大的模式如何帮助处理数据集。

现在我们已经看了这两种方法,让我们回到一些更有趣的东西,用一个新的,稍微更复杂的可视化。

使用 D3 实现甜甜圈图

下一个示例说明了另一种基本的数据可视化:在这种情况下,是一个甜甜圈图。比饼图稍微复杂一些,这个可视化展示了 D3 的一些新特性。完成后,可视化将如下截图所示。

它代表了个别漫画书(按标题和期号引用)在有史以来的前 50 本漫画书销售中的分布(公开销售,在撰写时)。像这样的列表中有一些主导的漫画书,这张图表将显示哪些最主导:

数据看起来像下面的 CSV:

title,numbers
"Action Comics #1",18
"All Star #8", 1
"Amazing Fantasy #15",4
"Batman #1",2
"Captain America Comics #1", 1
"Detective Comics #27",13
"Flash Comics #1", 2
"Incredible Hulk #1", 2
"Marvel Comics #1", 1
"Sensation Comics #1", 1
"Tales of Suspense #39", 1
"X-Men #1", 3

HTML 文件非常简单。它包括了Raleway,Bootstrap,d3-fetch和 D3 作为依赖项。它包括了我们在本书中几个示例中一直在工作的相同标记,然后包括我们的donut.js文件,这是一切有趣的地方:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- D3 Chord Diagram</title>
    <link rel="stylesheet" 
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
    <link href="https://fonts.googleapis.com/css?family=Raleway" 
     rel="stylesheet">
    <style type="text/css">
        text {
            font-family: Raleway;
            font-size: .8em;
            text-anchor: middle;
            fill: #fff;
        }
        text.legend{
            font-size: 1.25em;
            fill: #000;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-12" id="target">

            </div>
        </div>
    </div>

    <script src="img/d3.min.js"></script>
    <script src="img/d3-fetch.min.js"></script>
    <script src="img/donut.js"></script>

</body>

</html>

看着donut.js,有很多事情要做,所以我们将再次逐个部分地查看文件。如果您想看整个文件,请查看完整的源代码。

文件从设置可视化的heightwidthradius的几个常数开始。然后我们创建一个颜色比例尺,它经过 13 种蓝色和绿色的色调:

const width = 1000,
  height = 1000,
  radius = Math.min(width, height) / 2;

const color = d3.scaleOrdinal()
  .domain(d3.range(13))
  .range([
    "#1fb003",
    "#1CA212",
    "#199522",
    "#178732",
    "#147A41",
    "#126C51",
    "#0F5F61",
    "#0C5170",
    "#0A4480",
    "#073690",
    "#05299F",
    "#021BAF",
    "#000EBF"
  ]);

接下来的两个方法调用只是为了为以后的可视化设置。在这一点上,我们没有任何数据可以使用,但是当数据到达时,我们仍然可以创建一些加载的 D3 工具来处理数据。第一个常数arc将允许我们用outerRadius绘制弧,该弧接近 SVG 元素的边缘,并且innerRadiusouterRadius内 200 像素。这创建了一个 190 像素的环。

接下来我们调用d3.ie,这是一个接收数据并返回代表饼图或甜甜圈图的正确比例切片的方法。我们还没有数据,但是我们设置了该方法,以便在创建arc时使用数据对象的numbers属性:

const arc = d3.arc()
  .outerRadius(radius - 10)
  .innerRadius(radius - 200);

const pie = d3.pie()
  .value((d) => {
    return d.numbers;
  });

接下来我们开始实现一些 SVG。第一个调用到这个时候应该对你来说是常见的。我们调用d3.select来获取#target元素,然后将 SVG 元素附加到 DOM 中。然后我们使用attr来设置heightwidth,然后在 SVG 文档中附加一个组g元素。然后将该g转换为 SVG 元素的中心,通过将其平移半个宽度和半个高度。

接下来,我们在包含可视化的g元素中附加一个新的text元素,用于小传说:

let svg = d3.select("#target").append("svg")
  .attr("width", width)
  .attr("height", height)
  .append("g")
  .attr("transform", `translate(${width / 2},${height / 2})`);

svg.append("text")
  .text("Distribution of comic book titles in top 50 sales of all time.")
  .attr("class","legend");

现在我们已经完成了所有这些设置,是时候处理一些数据并绘制可视化了。我们首先使用d3-fetch中的另一个方法d3.csv,来获取包含我们的数据并在 D3 解析后处理它的 CSV 文件。

callback内部,有一个现在熟悉的 D3 模式。首先,调用svg.selectAll("arc"),这时返回一个空选择。然后我们调用data,传入pie(data)pie接收数据并返回我们用于甜甜圈图的起始和结束角度。接下来我们进入 enter 选择,并为每个选择的成员附加g元素。我们还没有画任何东西,但是我们已经为每个数据点设置了组,并且已经计算了应用于数据集的起始和结束角度。

下一节说明了与 D3 一起工作有多么美妙。

此时,我们已经得到了通过调用pie生成的角度,附加到许多空的g元素上。在下一个调用中,我们附加了一个path元素,并且通过调用先前创建的arc方法,将d属性填充为绘制可视化所需的完整arc。就是这么简单。

现在,对于图表本身,唯一剩下的就是通过从之前创建的颜色比例尺返回一个值来填充arc的颜色。这是基于数据的索引进行选择。数据根据其在漫画书标题中的排名进行排序。这样在运行此可视化时,我们看到了漂亮的渐变。如果你停在这里,你实际上已经有了一个可视化。它没有与之相关的任何文本,但你已经有了一个看起来不错的甜甜圈图。这就是 D3 的威力。

也就是说,我们应该添加一些标签,让我们看看它是如何工作的。初始模式是你应该开始熟悉的。我们调用selectAll(".label"),加载数据(通过对pie的另一个调用来操作,以获得相同的起始和结束角度),然后在 enter 选择中操作它。在 enter 选择中,我们附加一个text元素,然后采取几个步骤将文本放置在整个可视化中的有用位置。

第一步是使用arc.centroid方法将文本元素平移到arc的中心。同样,这是 D3 有多么有用的一个很好的例子。一个小小的调用就可以让你访问一个复杂形状的几何中心。这对大多数文本元素都适用。我们快要完成了。

我们只需要调整两种特定情况下的文本。没有下一个调用,最后几个元素的可视化中文本会以不美观的方式重叠,如下面的截图所示:

要调整这两个重叠元素的位置,我们需要找出它们是哪两个。我们知道它们是最后两个,并且它们会靠近圆圈的末端。这里的角度是用弧度来测量的(360 度是 2PI 或大约 6.28 弧度)。使用粗略的简写,一个切片(0.125 弧度大约代表我们可视化中的一个切片),我们从整个圆圈向后测试最后两个切片,并使用dy属性稍微调整它们。第一个通过.6em进行调整。接下来,最后一个文本元素通过1.5em进行调整。这意味着每个标签都清晰可读。

最终的调用实际上通过调用text并将数据的title作为参数将文本附加到元素中:

d3.csv("data/top-fifty-comics-data.csv").then((data) => {
  let g = svg.selectAll(".arc")
    .data(pie(data))
    .enter()
    .append("g");

  g.append("path")
    .attr("d", arc)
    .style("fill", (d) => {
      return color(d.index);
    });

  svg.selectAll(".label")
    .data(pie(data))
    .enter()
    .append("text")
    .attr("class", "text")
    .attr("transform", (d) => {
      return `translate(${arc.centroid(d)})`;
    })
    .attr("dy", (d) => {
      if (d.startAngle > 6.0 && d.startAngle < 6.125) {
        return "-.6em";
      } else if (d.startAngle > 6.125) {
        return "-1.5em";
      }
    })
    .text((d) => {
      return d.data.title;
    });

});

现在我们已经完成了两个标准图表,是时候做一个更有趣的弦图了。这个最终的例子将展示 D3 的更多特性。这会很有趣。

在 D3 中实现弦图

这个最终的可视化在数据和编码方面都更加复杂。该可视化基于几年前发布的数据,作为 Hubway 数据可视化挑战的一部分(hubwaydatachallenge.org/)。这是一个庞大的数据集,代表了波士顿的 Hubway 共享单车项目(现在称为 Blue Bikes)上的每一次行程,包括出发和到达站。这个可视化展示了波士顿十个最受欢迎站点之间的关系,说明了这些站点之间发生的行程数量。这很有趣,可以看到主要枢纽站之间的潜在公共交通网络漏洞(很多人在主要枢纽站之间出行,比如北站和南站),或者可能被游客用来观光波士顿(很多南站的行程返回到南站)。

最终的可视化看起来是这样的。每个arc代表一个出发站,两个站点之间的带状物显示了两个站点之间行程的相对权重。当它离开arc时的宽度代表行程的数量。arc的颜色由生成两个站点之间更多行程的站点拥有:

这个可视化的 HTML,就像其他 D3 示例一样非常简单。我们在头部有Raleway和 Bootstrap。然后顶部有一段 CSS 来添加一些文本样式,以及一个小的定义来为圆圈外缘显示刻度数字的小刻度添加描边颜色。

另外,有一个包含可视化描述的H1。然后我们只包含主要的 D3 文件和我们的可视化文件。所有重要的事情都发生在chord.js中:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Mastering SVG- D3 Chord Diagram</title>
    <link rel="stylesheet" 
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
    <link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet">
    <style type="text/css">
    h1 {
        font-family: Raleway;
    }
    text {
        font-family: Raleway;
        font-size: 1em;
    }
    text.tick {
        font-size: .8em;
        fill: #999;
    }
    .group-tick line {
        stroke: #999;
    }
    </style>
</head>

<body>
    <div class="container-fluid">
        <div class="row">
            <div class="col-12" id="target">
                <h1>Trip connections between the top 10 Hubway 
                    departure stations. Data from the
                    <a href="http://hubwaydatachallenge.org/">Hubway 
                     Data Visualization Challenge</a>
                </h1>

            </div>
        </div>
    </div>

    <script src="img/d3.min.js"></script>
    <script src="img/chord.js"></script>

</body>

</html>

让我们开始通过查看数据来看chord.js。文件顶部有整个可视化的硬编码数据。这是一个更大数据集的精简版本,在这个可视化的原始版本中,有很多代码用于创建特定格式的数据。生成这些数据的代码可以在 GitHub 上找到,以及本书的其他源代码。

弦图需要一个方阵。这是一个数组的数组,其中数组的成员总数与子数组的成员总数相匹配,并且你可以在它们之间进行映射。在我们的例子中,父数组代表一个出发站,子数组的值代表到达每个到达站的总行程数。子数组的索引与父数组的索引相匹配。一个出发站也可以是到达站。

names const 包含每个出发站的名称,与matrix数组中出发站的索引相匹配:

function drawChord() {
  const names = [
    "South Station",
    "TD Garden",
    "Boston Public Library",
    "Boylston St. at Arlington St",
    "Back Bay / South End Station",
    "Charles Circle",
    "Kenmore Sq / Comm Av",
    "Beacon St / Mass Av",
    "Lewis Wharf",
    "Newbury St / Hereford S"
  ];
  const matrix = [
    [2689, 508, 1170, 189, 1007, 187, 745, 248, 263, 2311],
    [1064, 121, 830, 323, 2473, 393, 453, 312, 533, 599],
    [506, 296, 813, 530, 988, 540, 1936, 578, 747, 268],
    [706, 311, 1568, 526, 1273, 371, 618, 694, 481, 227],
    [178, 701, 277, 176, 663, 227, 379, 284, 330, 111],
    [550, 270, 548, 445, 196, 769, 868, 317, 1477, 195],
    [344, 141, 468, 955, 172, 346, 502, 388, 415, 97],
    [333, 207, 455, 545, 196, 1322, 618, 254, 659, 62],
    [655, 120, 301, 90, 2368, 108, 226, 99, 229, 875],
    [270, 221, 625, 436, 239, 278, 548, 1158, 320, 90]
  ];

现在我们已经整理好了数据,让我们开始看看如何生成实际的可视化。前五个代码块都是用于设置。这是你通常使用 D3 做的事情,而这个比其他的更复杂,所以需要更多的设置。

第一个块只涉及对可视化所需的各种度量的常量的创建。widthheight对我们所有的 D3 示例都是常见的。radius是一个计算出的值,表示一个圆的完整半径,该圆可以适应由高度和宽度创建的正方形。padding常量用于计算可视化实际圆的outerRadius。然后我们使用outerRadius来计算innerRadius

接下来,我们将直接开始使用 D3。第一个调用是d3.chord,其结果存储在一个常量chord中。chord是一个加载方法,将使用我们的设置生成一个弦图。第一个设置padAngle是一个radians参数,表示arc之间的间距。对于这样一个复杂的可视化,arc之间有一点空间是很好的,以便为各个部分带来一些清晰度。第二个设置指示我们是否要对子组进行排序。在我们的情况下,我们需要,所以我们传入d3.descending作为预定义的排序。

下一个变量arc加载了一个d3.arc的实例,带有我们计算出的innerRadiusouterRadius,就像甜甜圈图表一样。一旦你开始把这些东西看作是可以组合在一起的组件,可能性就会打开。

接下来,我们将使用 D3 ribbon创建一个实例,innerRadius是唯一的配置设置,作为参数传递给radius方法。这个方法与chord方法一起使用,创建可视化的核心,连接连接的丝带的两端,在我们的例子中是出发和到达站。

最后,我们创建一个color比例尺,将车站映射到一组彩虹颜色:

const width = 1200,
    height = 1200,
    radius = Math.min(width, height) / 2,
    padding = 200,
    outerRadius = radius - padding,
    innerRadius = outerRadius - 25;

  const chord = d3.chord()
    .padAngle(0.025)
    .sortSubgroups(d3.descending);

  const arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

  const ribbon = d3.ribbon()
    .radius(innerRadius);

  const color = d3.scaleOrdinal()
    .domain(d3.range(9))
    .range([
      "#e6194b",
      "#ffe119",
      "#0082c8",
      "#f58231",
      "#911eb4",
      "#46f0f0",
      "#f032e6",
      "#d2f53c",
      "#808000",
      "#008080"
    ]);

现在我们已经设置好了,是时候开始在屏幕上进行可视化工作了。第一个块在这一点上应该非常熟悉。在其中,我们选择#target元素,附加一个 SVG 元素,然后设置它的widthheight

下一个块也应该大部分是熟悉的。在其中,我们向 SVG 元素添加一个g组,然后将其平移到屏幕的中心。这里有趣的部分是对datum的调用,这是一个非常类似于data的方法,除了它将数据传播到整个树中。在这里,我们传入我们的chord实例,以及我们的matrixchord方法返回我们数据可视化的构建块。

这一部分的最后一个块创建了将容纳我们的弧段、路径和组刻度的组。我们进入enter选择,并为matrix的每个项目附加一个子g元素:

  const svg = d3.select("#target")
    .append("svg")
    .attr("height", height)
    .attr("width", width);

  const g = svg.append("g")
    .attr("transform", `translate(${width / 2},${height / 2})`)
    .datum(chord(matrix));

  const group = g.append("g")
    .attr("class", "groups")
    .selectAll("g")
    .data((chords) => chords.groups)
    .enter()
    .append("g");

在这一点上,我们已经完成了所有的设置。现在是时候真正地在屏幕上绘制一些元素了。

添加到可视化中的第一个部分是arc。这个模式对你来说应该很熟悉,来自甜甜圈图表。这完全相同的模式;只是这里它是更大的可视化的一部分。

group变量已经是一个Enter选择的一部分,因此这一部分和我们添加图例的下一部分已经在完整的数据集上运行。

首先我们附加一个path,并使用我们对arc的调用结果设置pathd属性。这返回了切片的起始和结束角度。然后我们给它一个fill和一个strokestroke提供了 D3 的另一个实用工具的首次亮相。D3.color (github.com/d3/d3-color)提供了几种选项来处理颜色。在这里,我们使用d3.color.darker来返回所选“弧”的略暗色,以便给它足够的对比度来显示边缘。最后,我们添加了两个事件处理程序,允许用户在鼠标悬停在该站点的弧上时淡化所有其他站点的弧和带。这将使他们能够检查特定站点的连接,而不会受到其他站点的干扰。我们稍后会详细讨论这个功能。

接下来我们添加了带。这与“弧”非常相似。我们从核心g组开始,附加一个新的带组,添加一个带的类。然后我们调用selectAll("path")来进行选择,调用data来应用弦数据,然后我们进入enter选择来构建带。对于数据集的每个成员,我们附加一个新的path,并使用ribbon的调用设置路径的d属性。ribbon的返回值创建了一个连接“弧”一侧的两个角度与“弧”另一侧的两个角度的路径。之后,我们以与弧相同的方式设置strokefill,以便一切匹配:

  group.append("path")
    .attr("d", arc)
    .style("fill", (d) => color(d.index))
    .style("stroke", (d) => d3.color(color(d.index)).darker())
    .on("mouseover", fade(.1))
    .on("mouseout", fade(1));

  g.append("g")
    .attr("class", "ribbons")
    .selectAll("path")
    .data((chords) => chords)
    .enter()
    .append("path")
    .attr("d", ribbon)
    .style("fill", (d) => color(d.source.index))
        .style("stroke", (d) => {
      return d3.color(color(d.source.index)).darker();
    });

此时,可视化已经绘制到屏幕上。不过我们仍然可以做得更好,所以让我们来做吧。

接下来的部分为每个站点添加了小标签。与之前一样,我们已经处于“enter”选择中,因此我们已经在正确的数据集上操作。这个链中的第一个调用是each,它允许我们在选择的每个成员上运行一个函数。传入的callback函数添加了一个新的属性到数据集,即angleangle通过将“弧”的起始角度和结束角度相加并除以 2 来计算得到,得到“弧”的中间部分。我们将使用该角度来在下一个调用中放置标签。

我们用甜甜圈图表做的标签放在了“弧”上。这在我们设置的弦图表和我们拥有的长文本标签上实际上看起来并不那么好,所以我们想把标签移到圆圈外。我们用一些三角学来实现这一点。

以下图表显示了这是如何工作的。所有这些text元素都在 SVG 元素的中心的一个组中。我们要将它们移动到它们的新位置,即圆圈外与它们标记的弧的中间对齐。为此,我们取我们计算的d.angle属性,并将其用作直角三角形的斜边(最长边)。一旦我们得到了那个角度,我们就可以计算正弦(对边长与斜边长的比值)和余弦(邻边长与斜边长的比值)。一旦我们有了这些比值,我们只需将它们乘以outerRadius(再加上一些额外像素以给它一些空间)就可以得到三角形的邻边和对边的长度。我们将这些值用作将文本元素转换到它们的新位置所需的xy

这项技术将随时派上用场:

接下来的部分根据arc上文本元素的位置调整text-anchor属性。如果它大于一半(圆上有两个 PI 弧度,所以Math.PI相当于圆的一半),那么我们需要将text-anchor设置为end,以便与圆右侧的标签平衡。如果我们不以这种方式调整 text-anchor,可视化的左侧文本元素将与该侧的弧重叠。

最后,我们附加文本本身:

group.append("text")
    .each((d) => d.angle = (d.startAngle + d.endAngle) / 2)
    .attr("text-anchor", (d) => {
      if (d.angle > Math.PI) {
        return "end";
      }
    })
    .attr("transform", (d) => {
      const y = Math.sin(d.angle) * (outerRadius + 10),
        x = Math.cos(d.angle) * (outerRadius + 20);
      return `translate(${y},${(-x)})`;
    })
    .text((d) => {
      return names[d.index];
    });

我们要为这个可视化添加的最后的 SVG 元素是在外边缘添加组刻度和刻度标签。这些将允许我们以友好的方式指示可视化的规模,以千为单位。

我们首先创建一个新的常量groupTick,它基于对groupTicks方法的调用返回的数据设置了一个新的进入选择。groupTick接收链中的现有数据,并返回一个新的操纵后的数据集,代表每 1000 个单位的新刻度。这些新的groupTick数据条目具有一个新的角度,对应于刻度在弧上的正确位置,并引用原始数据的value。一旦groupTick数据返回,我们进入选择,附加一个新的组并添加一个类group-tick。然后我们将元素旋转以在外边缘形成视觉圆圈,并将其平移到outerRadius的一个点。

一旦完成,我们在每个刻度处添加一个六像素长的灰色“线”。记住,groupTick仍然在这个新链中的一个进入选择中,所以即使我们打破了之前的链,我们仍然可以操作每个数据点。

最后,我们再次进入选择并filter数据,防止空数据,然后测试值是否可以被 5000 整除,使用模数(或余数)运算符(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Remainder_())。如果可以被 5000 整除,我们需要添加一些文本以指示我们已经为该站点完成了 5000 次行程。这样做的步骤如下。调整x属性,将其移动到outerRadius之外。调整dy属性,将文本元素向上移动一点,以更好地与刻度线对齐。

如果角度超过圆的一半,则转换text元素。再次对Math.PI进行测试,然后,如果超过一半,我们将文本旋转 180 度,然后将其平移 16 像素,以使其完美地贴合outerRadius的边缘。我们还对text元素是否超过圆的一半进行相同的测试,如果是,我们将text-anchor属性更改为将文本的右边缘固定在圆的边缘。最后,我们向text元素添加一个类ticks,并使用d3.formatPrefix附加实际文本。d3.formatPrefix根据提供的格式化参数格式化数字,使其更友好。

在这种情况下,我们希望使用 SI(国际单位制)前缀(en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes)格式化数字,这将把5000转换为5k

  const groupTick = group.selectAll(".group-tick")
    .data((d) => groupTicks(d, 1000))
    .enter()
    .append("g")
    .attr("class", "group-tick")
    .attr("transform", (d) => {
      return `rotate(${(d.angle * 180 / Math.PI - 90)}) translate(${outerRadius},0)`;
    });

  groupTick.append("line")
    .attr("x2", 6);

  groupTick
    .filter((d) => d.value && !(d.value % 5000))
    .append("text")
    .attr("x", 8)
    .attr("dy", ".35em")
    .attr("transform", (d) => {
      if (d.angle > Math.PI) {
        return "rotate(180) translate(-16)";
      }
    })
    .style("text-anchor", (d) => {
      if (d.angle > Math.PI) {
        return "end";
      }
    })
    .attr("class", "tick")
    .text((d) => d3.formatPrefix(",.0", 1000)(d.value));

  function groupTicks(d, step) {
    let k = (d.endAngle - d.startAngle) / d.value;
    return d3.range(0, d.value, step).map((value) => {
      return {
        value: value,
        angle: value * k + d.startAngle
      };
    });
  }

最后的代码是之前提到的fade方法。这个函数选择与 CSS 选择器匹配的所有元素。.ribbons path过滤掉与当前选择相关的任何元素,并将它们的opacity设置为提供的opacity参数:

  function fade(opacity) {
    return function(g, i) {
      svg.selectAll(".ribbons path")
        .filter((d)=> {
          return d.source.index !== i && d.target.index !== i;
        })
        .transition()
        .style("opacity", opacity);
    };
  }

效果如下截图所示:

有了这个,弦图就完成了。这并不是你在 D3 中见过的最复杂的可视化,但它还是相当不错的。连同甜甜圈图和条形图,这三种图表结合起来展示了 D3 的许多重要特性。

总结

本章向您介绍了 D3 的世界。尽管本章深入,但只是触及了 D3 所提供的一部分。希望您能将在这里学到的知识继续在未来的几个月和几年中进行实验。这是一个值得掌握的有益工具。

在本书中,我们只剩下一个简短的章节,我们将讨论一些优化 SVG 在网络上提供的方法。这是一个至关重要的领域,至少应该有一些了解,特别是如果您在您的网站或应用程序中使用了大量的 SVG。

第十一章:优化 SVG 的工具

现在您已经在本书中学习了关于 SVG 的一切,从纯 SVG 标记的基础知识到过去几章中您所做的基于动态 JavaScript 的 SVG 工作,您已经准备好充分利用 SVG 所提供的一切。

我们应该看一下 SVG 的最后一个方面,即确保您提供给用户的工作以最佳方式呈现。SVG 应该针对性能进行优化,无论是在传输过程中的性能还是在复杂性方面。保持 SVG 文件尽可能精简并有效地提供它们将为用户带来更好的体验。

本章将作为一个高层次的介绍,向您展示优化 SVG 图像的许多方法。接下来的内容有些是纯粹与性能相关的工程。其他则是纯 SVG 工具。

本章中,您将了解以下内容:

  • 在三种流行的服务器平台(IIS、Apache 和 nginx)上对服务器上的 SVG 进行压缩

  • SVGO 及其相关工具

  • svgcleaner,SVGO 的替代方案,提供无损优化

提供压缩的 SVG

在处理 SVG 时,最直接的性能增强之一就是在提供文件时对gzip文件进行压缩。虽然文本文件通常在提供给浏览器时受益于被 gzipped,但 SVG 是一个特别重要的目标,因为 SVG 图像的使用方式(通常用于核心界面)以及一些文件的潜在大小。您希望您的图像加载速度快,SVG 也不例外。

根据您的平台,这可能只需添加几行代码或在对话框中勾选一个框。接下来的几节将向您展示如何在三种常见的 Web 服务器上实现此操作。

在 Apache 上对 SVG 进行 gzip 压缩

放置以下代码的位置取决于您的 Apache 实例设置以及您对服务器的访问权限。大多数共享主机的用户将在他们的.htaccess文件中执行此操作。.htaccess是服务器根目录中的一个特殊文件,允许您在不必访问主配置文件(httpd.conf)的情况下配置 Apache 行为。假设您的服务器允许您访问此功能(一些主机不允许您打开压缩,因为它会使用更多的服务器资源),则将文本内容进行 gzip 压缩就像在您的.htaccess文件中添加以下内容一样简单。示例代码来自 H5BP Apache 服务器配置项目(github.com/h5bp/server-configs-apache/blob/master/dist/.htaccess#L795)。有三个部分:

  • 第一个修复了代理服务器搞乱请求标头导致资源无法被提供为 gzipped 的问题(这不仅修复了 SVG)

  • 第二个实际上告诉 Apache 压缩列出的 MIME 类型的文件(这里进行了缩写;通常会列出几十种不同的 MIME 类型)

  • 第三个确保以压缩格式.svgz压缩并保存的 SVG 文件能够正确提供:

# ######################################################################
# # WEB PERFORMANCE #
# ######################################################################

# ----------------------------------------------------------------------
# | Compression |
# ----------------------------------------------------------------------

<IfModule mod_deflate.c>

    # Force compression for mangled `Accept-Encoding` request headers
    #
    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
    # https://calendar.perfplanet.com/2010/pushing-beyond-gzipping/

    <IfModule mod_setenvif.c>
        <IfModule mod_headers.c>
            SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
            RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
        </IfModule>
    </IfModule>

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Compress all output labeled with one of the following media types.
    #
    # https://httpd.apache.org/docs/current/mod/mod_filter.html#addoutputfilterbytype

    <IfModule mod_filter.c>
        AddOutputFilterByType DEFLATE "application/atom+xml" \
                                      "application/javascript" \
                                      "application/json" \
# Many other MIME types clipped for brevity
                                      "image/svg+xml" \
# Many other MIME types clipped for brevity        
                                      "text/xml"

    </IfModule>

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Map the following filename extensions to the specified
    # encoding type in order to make Apache serve the file types
    # with the appropriate `Content-Encoding` response header
    # (do note that this will NOT make Apache compress them!).
    #
    # If these files types would be served without an appropriate
    # `Content-Enable` response header, client applications (e.g.:
    # browsers) wouldn't know that they first need to uncompressed
    # the response, and thus, wouldn't be able to understand the
    # content.
    #
    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
    # https://httpd.apache.org/docs/current/mod/mod_mime.html#addencoding

    <IfModule mod_mime.c>
        AddEncoding gzip svgz
    </IfModule>

</IfModule>

在 nginx 上对 SVG 进行压缩

与 Apache 类似,为 SVG 打开gzip压缩只是配置几行代码的问题。这段代码块来自 HTML5 锅炉板 nginx 服务器配置项目(github.com/h5bp/server-configs-nginx/blob/master/nginx.conf#L89),提供了如何执行此操作的示例。该代码将打开gzip压缩,设置gzip压缩级别,停止对已经很小的对象进行压缩,为代理设置一些值,然后将 SVG MIME 类型添加到应该被压缩的对象列表中(这里进行了缩写;通常会列出几十种不同的 MIME 类型):

# Enable gzip compression.
  # Default: off
  gzip on;

  # Compression level (1-9).
  # 5 is a perfect compromise between size and CPU usage, offering about
  # 75% reduction for most ASCII files (almost identical to level 9).
  # Default: 1
  gzip_comp_level 5;

  # Don't compress anything that's already small and unlikely to shrink much
  # if at all (the default is 20 bytes, which is bad as that usually leads to
  # larger files after gzipping).
  # Default: 20
  gzip_min_length 256;

  # Compress data even for clients that are connecting to us via proxies,
  # identified by the "Via" header (required for CloudFront).
  # Default: off
  gzip_proxied any;

  # Tell proxies to cache both the gzipped and regular version of a resource
  # whenever the client's Accept-Encoding capabilities header varies;
  # Avoids the issue where a non-gzip capable client (which is extremely rare
  # today) would display gibberish if their proxy gave them the gzipped version.
  # Default: off
  gzip_vary on;

  # Compress all output labeled with one of the following MIME-types.
  # text/html is always compressed by gzip module.
  # Default: text/html
  gzip_types
    # Many other MIME types clipped for brevity
    image/svg+xml
    # Many other MIME types clipped for brevity

IIS 上的 SVG 压缩

IIS 默认情况下不会压缩 SVG 文件。根据服务器的配置方式,需要在applicationHost.configC:\Windows\System32\inetsrv\config)或web.config文件中进行以下更改。您只需将 SVG MIME 类型添加到httpCompression模块中的staticTypesdynamicTypes元素中,然后就可以开始了:

<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
    <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
        <staticTypes>
            <add mimeType="image/svg+xml" enabled="true" />
        </staticTypes>
        <dynamicTypes>
            <add mimeType="image/svg+xml" enabled="true" />
        </dynamicTypes>
</httpCompression>

现在我们已经学会了有效地提供 SVG,是时候看看一些在将 SVG 放在服务器上之前对 SVG 进行优化的方法了。

SVGO

SVG 优化器(github.com/svg/svgo)是用于优化 SVG 文件的 Node.js 实用程序。SVG 文件,特别是由编辑器生成的文件,可能会有许多与之相关的垃圾。SVGO 可以清理元数据、注释、隐藏元素等,而不会改变 SVG 元素本身的渲染。

要安装它,假设您已安装了 Node.js,请在命令行上运行以下命令:

$ npm install -g svgo

使用方法就是这么简单:

svgo svgo.svg

在 Inkscape 生成的小文件上运行,可以将文件大小减少 50%以上:

如果您查看svgo.svg源代码在优化之前和之后的变化,差异是显而易见的。

以下截图显示了在创作过程中 Inkscape 添加的元数据:

此截图显示了优化后的清理文件:

这是一个很棒的工具,有许多配置选项(github.com/svg/svgo#usage)和与其他工具的集成(github.com/svg/svgo#other-ways-to-use-svgo)。

SVGOMG

在前面的链接中列出的集成之一是与 SVGO 的 Web 前端 SVGOMG 的集成(jakearchibald.github.io/svgomg/)。SVGOMG 是 SVGO 的 Web 前端。在 UI 中几乎暴露了所有选项,使您能够更深入地了解 SVGO 提供的优化,而无需研究所有配置选项。将 SVG 元素加载到界面中,会呈现以下视图:

加载的 SVG 在左侧,显示为优化视图。您可以切换“显示原始”按钮,以查看由于优化而导致的可见图像是否有任何降级。

请记住,SVGO 提供的一些优化可能会有损失。这意味着图像本身可能会以某种可见的方式发生变化;由于运行的优化,图像的有效数据将丢失。

然后,在右侧有许多可供您调整图像的选项。有一个节省的预览,然后有一个下载按钮,可以让您下载您的作品。

尽管许多人会将此优化作为构建过程的一部分自动化,但知道您可以在 Web 上对此工具进行精细控制,并立即获得更改的反馈是件好事。

SVGO 创作插件

除了可用的命令行工具和基于 Web 的界面之外,还有一些创作插件可供您将 SVGO 直接集成到创作工作流程中。SVG-NOW是 Adobe Illustrator 的插件(尽管它似乎已被放弃;自 2014 年以来就没有更新过),而 SVGO Compressor 是流行应用 Sketch 的一个正在积极开发的插件。如果您有一个设计团队,您可以通过在生产过程中较早地集成这些优化来节省时间并避免出现意外。由于他们将控制导出过程,他们将准确知道 SVGO 优化的输出将是什么。

svgcleaner

svgcleaner 是 SVGO 的替代品,提供无损优化github.com/RazrFalcon/svgcleaner)。与有可能破坏事物的 SVGO 相比,svgcleaner 承诺永远不会破坏 SVG 文件。浏览他们的图表(github.com/RazrFalcon/svgcleaner#charts)以查看他们如何与 SVGO 和 scour(另一种替代品)进行比较。

此外,还有一个可下载的 GUI(github.com/RazrFalcon/svgcleaner-gui/releases),您可以在桌面上运行。以下截图显示了它的运行情况。要达到这种状态所发生的一切就是加载一个 SVG 元素并点击播放按钮,这将运行优化:

由于它是用 Rust 构建的,而不是原生的 Node.js 应用程序,它与npm/node世界的兼容性不是很好,但它仍然是一个很棒的工具。

总结

这是本书中最轻松的一章,但您仍然学到了一些有助于 SVG 优化的知识。牢记这些因素和这些工具将确保用户获得最佳的结果,并确保您对 SVG 的辛勤工作能够以最佳的方式展现出来。

有了这一点,我们对 SVG 世界的旅程就结束了。从最基本的 SVG 元素,到复杂的 JavaScript 可视化和基于 CSS 的动画,您体验了 SVG 所能提供的全部广度。希望您享受了这段旅程,并将继续在未来与 SVG 一起工作。

posted @ 2024-05-24 11:14  绝不原创的飞龙  阅读(25)  评论(0编辑  收藏  举报