前面我们使用的是声明式动画,下一步我们将换成脚本来处理动画,这也是一大步改变。你可以使用 ECMAScript 编程来与 SVG 图形进行交互。[ECMAScript 是 JavaScript 标准化版本,由 ECMA 组织(European Computer Manufacturer's Association,欧洲计算机制造联合会)定义。] 如果你没有接触过 ECMA/JavaScript,或者没有接触过编程,可以参考附录 C。

SVG 阅读器在读取 SVG 文档的标记时,会创建一棵节点树,也就是内存中的一些对象,它们和标记的结构、内容一一对应。这就是 DOM(Document Object Model,文档对象模型),你可以通过脚本来访问。

在你处理 DOM 之前,首先要做的就是获取到节点。最主要的方法就是 document.getElementById(idString)。这个方法接受一个字符串类型的参数,代表 SVG 元素的 id,返回 DOM 中对应节点的引用。如果你想要获取文档中标记名(标记名指 <svg> 中的 svg<rect> 中的 rect 等)为某个名字的所有元素,可以使用 document 上的另一个方法 getElementsByTagName(name),它返回一个节点数组。

当你获取到元素的节点之后,就可以:

  • 使用 element.getAttribute(attributeName) 读取它的属性,以字符串形式返回属性的值;

  • 使用 element.setAttribute(name, newValue) 改变属性值,如果指定名称的属性不存在,则会创建;

  • 使用 element.removeAttribute(name) 删除属性。

你可以使用 element.setAttribute("style", newStyleValue) 来修改内联样式,但这样会覆盖元素上的所有样式。为了防止样式被覆盖,你可以使用 element.style 属性。

  • element.style.getPropertyValue(propertyName) 获取指定样式。

  • element.style.setProperty(propertyName, newValue, priority) 修改属性(priority 通常为 null,但也可以是 important)。

  • element.style.removeProperty(propertyName) 删除属性。

如果你真的希望一次设置所有的样式,也可以直接修改 element.style.cssText,这个属性是一个代表所有样式的字符串,格式为 property-name: value3

3大部分浏览器允许你通过 element.style.propertyName 或者 element.style["property-name"] 读写样式属性,但这并不属于 CSS 对象模型标准(http://www.w3.org/TR/cssom/),在一些 SVG 阅读器中也不支持,或者支持的情况不一致。

如果你需要获取或者修改节点的文本内容,使用 element.textContent 属性。读取该属性时,它会返回节点所有后代的文本拼接后的字符串。修改该属性时,则会用一个文本块替换所有的后代节点。4

4如果你对使用 .innerHTML 属性来修改组合文本和标记所有子元素很熟悉的话,请注意,该属性只在 HTMLElement 中被定义,很多浏览器和 SVG 阅读器在 SVG 元素上不支持该方法。

示例 13-5 使用了这些方法来获取 SVG 元素的属性,以文本方式显示它们,然后修改某个属性。(这个例子还没有交互,但对我们来说很重要,因为它是更多交互的基础。)

示例 13-5:使用 DOM 获取 SVG

http://oreillymedia.github.io/svg-essentials-examples/ch13/basic_dom_example.svg

<svg width="300" height="100" viewBox="0 0 300 100"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink">

  <title>Accessing Content in SVG</title>

  <rect id="rectangle" x="10" y="20" width="30" height="40"
    style="stroke:gray; fill: #ff9; stroke-width:3"/> ➊
  <text id="output" x="10" y="80" style="font-size:9pt"></text>

  <script type="application/ecmascript">
  // <![CDATA[ ➋
    var txt = document.getElementById("output"); ➌
    var r = document.getElementById("rectangle");
    var msg = r.getAttribute("x") + ", " +  ➍
      r.getAttribute("y") + " " +
      r.style.getPropertyValue("stroke") + " " +
      r.style.getPropertyValue("fill");
    r.setAttribute("height", "30"); ➎
    txt.textContent= msg; ➏
    // ]]>
  </script>
</svg>

❶ 为了更容易从脚本中获取元素,给它一个唯一的 id

<![CDATA[ 用于保证代码中的 <> 不被解析为标记。

❸ 从文档中通过 id 选择元素,并将结果保存在变量中。

getAttribute()style.getPropertyValue() 的结果是字符串,使用 + 将它们连接在一起构成消息字符串。

❺ 修改矩形的高度,让它变成正方形。

❻ 最后,设置 <text> 元素的内容来显示属性和属性值。

 示例 13-5 中的脚本是在 <rect><text> 元素之后引入到 SVG 文件中的,这样可以保证在脚本运行之前,元素已经在文档中存在。

当图形对象响应事件的时候就产生了交互。事件的类型有很多种。下方的事件描述有很多来自万维网联盟的规范(http://www.w3.org/TR/SVG/interact.html#SVGEvents)。

  • 用户接口事件

    当元素接受焦点和失去焦点(比如选中和取消选中文本)时会分别触发 focusInfocusOut 事件。当一个元素通过鼠标点击或者键盘操作激活时,会变成 activate 元素。

  • 鼠标事件

    当使用指针设备(如鼠标)在某个元素上点击和释放时会分别触发 mousedownmouseup 事件。如果这两个事件在屏幕上的位置相同,则会触发 click 事件。

    当指针设备移动进入某个元素、在元素内移动、从元素中移走时,会分别触发 mouseovermousemovemouseout 事件。

  • DOM 变化(mutation)事件

    当 DOM(被其他脚本改动导致)变化时,SVG 阅读器会触发一些事件。比如,当一个节点被添加到另一个节点作为子节点时,会触发 DOMNodeInserted 事件;当节点的属性发生变化时,会触发 DOMAttrModified 事件。本书不会详细描述与这些事件有关的内容。

  • 文档事件

    当 SVG 阅读器完全解析完文档,并准备好做进一步操作(比如显示到设备上)时,会触发 SVGLoad 事件。当文档被从窗口中移除时,会触发 SVGUnload 事件。当页面正在加载时被突然中止,会触发 SVGAbort 事件。当文档不能正确加载或者脚本执行有错误时,会触发 SVGError 事件。

    当阅读器文档的大小、滚动位置、缩放比例发生变化时,会触发 SVGResizeSVGScrollSVGZoom 事件。

  • 动画事件

    动画开始、结束和重复播放时会触发 beginEventendEventrepeatEvent 事件。repeatEvent 在第一次播放时不会触发。

  • 键盘事件

    SVG 并没有原生的键盘事件,但是有一些浏览器可能支持 keydownkeyup 事件,这是不标准的。

要让一个对象响应事件,首先需要让对象监听事件。我们通过 addEventListener() 函数来监听事件,它接受两个参数。第一个参数是表示要监听的事件类型的字符串。第二个参数是处理事件的函数。第三个参数是可选的,用来表示是否响应“事件捕获”阶段的事件。事件捕获是指阅读器将事件从根元素传递到子元素直到找到指定事件目标的过程。指定 false(通常用 false,也是默认值)表示响应“事件冒泡”阶段的事件。事件冒泡是指事件从触发事件的子元素一直传递到目标元素,再传递到根元素的过程。5

5这是一个很简单的定义,详情请参阅 DOM 事件标准(http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)。

事件处理函数接受一个参数:一个包含了触发函数调用的事件的相关信息的事件对象。事件对象中最重要的属性是 target 属性,它代表事件产生的元素。还有一些重要的属性,比如 clientXclientY,给出了事件发生的坐标,这个坐标是相对于整个 SVG 文件或者整个 Web 页面而言的。

示例 13-6 监听了圆上的 mouseovermouseoutclick 事件。鼠标移入移出圆会分别导致圆角半径的增和减,点击鼠标则会增或减圆的轮廓宽度。

示例 13-6:监听鼠标运动

http://oreillymedia.github.io/svg-essentials-examples/ch13/simple_event.svg

<circle id="circle" cx="50" cy="50" r="20"
  style="fill: #ff9; stroke:black; stroke-width: 1"/>

<script type="application/ecmascript"><![CDATA[
  function grow(evt) {
    var obj = evt.target;
    obj.setAttribute("r", "30");
  }

  function shrink(evt) {
    this.setAttribute("r", "20");
  }

  function reStroke(evt) {
    var w = evt.target.style.getPropertyValue("stroke-width");
    w = 4 - parseFloat(w); /* toggle between 1 and 3 */
    evt.target.style.setProperty("stroke-width", w, null);
  }

  var c = document.getElementById("circle");
  c.addEventListener("mouseover", grow);
  c.addEventListener("mouseout", shrink);
  c.addEventListener("click", reStroke);
  // ]]>

</script>

第一个事件处理函数 grow() 使用了 evt.target 来获取接收事件的元素,然后将它存在变量中。第二个事件处理函数 shrink() 使用了关键字 this 来引用跟事件监听函数绑定的元素(在这个例子中,和 evt.target 相同,但并不总是这样)。最后一个事件处理函数 reStroke() 也使用了 evt.target,但没有使用临时变量。

我们也可以使用 c 来代替 evt.target(因为这是唯一绑定事件处理函数的元素),但这是一种非常不好的办法,因为你经常需要将同一个事件处理函数绑定到不同的元素上,比如下一个例子。

有时候你会希望在对象 A 上产生的事件可以同时影响对象 A 和对象 B 的属性。示例 13-7 展示的可能是世界上最丑的电商网站了。在图 13-3 中展示了一件 T 恤,当用户点击选择尺码的时候,T 恤的尺寸也会随之改变。当前选中的尺码按钮会高亮成淡黄色背景。

{%}

图 13-3:不同尺寸选择的截图

示例 13-7:使用脚本修改多个对象

http://oreillymedia.github.io/svg-essentials-examples/ch13/shirt1.svg

XML 代码

<svg width="400" height="250" viewBox="0 0 400 250"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  onload="init(evt)"> ➊

  <defs>
    <style type="text/css" > <![CDATA[
      /* style rules will go here */
    ]]></style>
    <script type="application/ecmascript"> <![CDATA[
      /* script will go here */
    ]]></script>

    <path id="shirt-outline"
      d="M -6 -30 -32 -19 -25.5 -13 -22 -14 -22 30 23 30
        23 -14 26.5 -13 33 -19 7 -30
        A 6.5 6 0 0 1 -6 -30"/> ➋
  </defs>

  <g id="shirt" >
    <use xlink:href="#shirt-outline" x="0" y="0"/>
  </g>

  <g id="scale0" >
    <rect x="100" y="10" width="30" height="30" />
    <text x="115" y="30">S</text>
  </g>

  <g id="scale1" class="selected"> ➌
    <rect x="140" y="10" width="30" height="30" />
    <text x="155" y="30">M</text>
  </g>

  <g id="scale2" >
    <rect x="180" y="10" width="30" height="30" />
    <text x="195" y="30">L</text>
  </g>
</svg>

❶ 当文档加载完成后,会触发 SVGLoad 事件,onload 函数会调用初始化函数,并传入事件信息。很多脚本都会这样处理,以确保所有的变量都被正确地初始化。这也使得你可以将 <script> 标记放到 SVG 元素之前,以方便维护。使用 oneventname 属性的形式也可以监听很多事件,但是不建议这样做(应该使用 addEventListener()),因为这样会将脚本函数和 XML 结构混合起来。不过文档加载事件是个例外。

❷ T 恤的轮廓中心是 (0,0),然后通过变换移到对应的位置,所以它的缩放中心点就是 T 恤中心。

❸ 中间的按钮在初始化时被选中。

样式

svg { /* 默认值 */
   stroke: black;
   fill: white;
}
g.selected rect {
   fill: #ffc; /* 淡黄色 */
}
text {
   stroke: none;
   fill:black;
   text-anchor: middle;
}

selected class 被用来标识哪个尺寸是被选中的,样式为淡黄色背景。

脚本

var scaleChoice = 1;  ➊
var scaleFactor = [1.25, 1.5, 1.75];

function init(evt) { ➋
  var obj;
  for (var i = 0; i < 3; i++) {
    obj = document.getElementById("scale" + i);
    obj.addEventListener("click", clickButton, false);
  }
  transformShirt();
}

function clickButton(evt) {
  var choice = evt.target.parentNode; ➌
  var name = choice.getAttribute("id");
  var old = document.getElementById("scale" + scaleChoice);
  old.removeAttribute("class"); ➍
  choice.setAttribute("class", "selected");
  scaleChoice = parseInt(name[name.length - 1]); ➎
  transformShirt();
}

function transformShirt() { ➏
  var factor = scaleFactor[scaleChoice];
  var obj = document.getElementById("shirt");
  obj.setAttribute("transform",
    "translate(150, 150) " +
    "scale(" + factor + ")");
  obj.setAttribute("stroke-width",
    1 / factor);
}

❶ 这段脚本会关注哪个按钮(S、M、L)被选择了,然后根据索引从 scaleFactor 数组中取到对应的缩放值。默认的索引为 1,代表中号(M)。

init() 函数会获取每个由矩形和文本组成的 <g>,并让它们监听 click 事件。然后在事件处理函数中让 T 恤显示正确的大小。

❸ 点击事件可能发生在文本上,也可能发生在矩形中。使用 parentNode 可以保证 choice 代表的是 <g> 对象。

❹ 将之前尺寸对应的按钮 <g> 上的 selected class 移除掉,然后为新选择的 <g> 添加这个 class。

❺ 通过按钮 <g>id 的后半段提取出数字,赋值给 scaleChoice。这是个全局变量,稍后可以被 transformShirt() 函数获取到。

❻ 改变 T 恤的大小和位置是通过设置它的 transform 属性完成的。轮廓的宽度则是通过取缩放值的倒数来指定,这样可以保证当 T 恤放大和缩小时,轮廓在视觉上是一样宽的。

我们来扩展这个示例,添加一些可以拖动的滑块,来设置 T 恤的颜色。如图 13-4 所示。

{%}

图 13-4:颜色滑块截图

你可以在线上示例中体验这些滑块:

http://oreillymedia.github.io/svg-essentials-examples/ch13/drag_objects.svg

这个示例的脚本需要更多的全局变量。首先是 slideChoice,用于标识当前被拖动的是哪一个滑块(012)。它的初始值是 -1,表示当前没有滑块被拖动。还需要使用一个 rgb 数组变量来分别保存红、绿、蓝的值(百分比),初始值都是 100,因为 T 恤一开始是白色的:

var slideChoice = -1;
var rgb = [100, 100, 100];

接下来绘制滑块。滑块的背景和上面指示当前值的线都是在白色背景上绘制的,然后将它们分组放在一起。id 属性放到 <line> 元素上,因为它的 y 坐标会变化。事件处理函数会被绑定到 <g> 上。这样这个组就会捕获发生在任何子元素上的鼠标事件。(这也是我们要画外面的白色框的原因;当鼠标移出背景外的时候,仍然可以跟踪到它的位置。)

<g id="sliderGroup0" transform="translate( 230, 10 )">
  <rect x="-10" y="-5" width="40" height="110"/>
  <rect x="5" y="0" width="10" height="100" style="fill: red;"/>
  <line id="slide0" class="slider"
    x1="0" y1="0" x2="20" y2="0" />
</g>

<g id="sliderGroup1" transform="translate( 280, 10 )">
  <rect x="-10" y="-5" width="40" height="110"/>
  <rect x="5" y="0" width="10" height="100" style="fill: green;"/>
  <line id="slide1" class="slider"
    x1="0" y1="0" x2="20" y2="0" />
</g>

<g id="sliderGroup2" transform="translate( 330, 10 )">
  <rect x="-10" y="-5" width="40" height="110"/>
  <rect x="5" y="0" width="10" height="100" style="fill: blue;"/>
  <line id="slide2" class="slider"
    x1="0" y1="0" x2="20" y2="0" />
</g>

新的样式规则包含了滑块所要的所有样式,只是没有指定它的颜色:

line.slider {
   stroke: gray;
   stroke-width: 2;
}

init() 函数分别为三个滑块添加了三个事件监听:

obj = document.getElementById("sliderGroup" + i);
obj.addEventListener("mousedown", startColorDrag, false);
obj.addEventListener("mousemove", doColorDrag, false);
obj.addEventListener("mouseup", endColorDrag, false);

对应的函数如下:

function startColorDrag(evt) { ➊
  var sliderId = evt.target.parentNode.getAttribute("id");
  endColorDrag( evt );
  slideChoice = parseInt(sliderId[sliderId.length - 1]);
}

function endColorDrag(evt) { ➋
  slideChoice = -1;
}

function doColorDrag(evt) { ➌
  var sliderId = evt.target.parentNode.getAttribute("id");
  chosen = parseInt(sliderId[sliderId.length - 1]);

  if (slideChoice >= 0 && slideChoice == chosen) {  ➍

    var obj = evt.target; ➎
    var pos = evt.clientY - 10;
    if (pos < 0) { pos = 0; }
    if (pos > 100) { pos = 100; }

    obj = document.getElementById("slide" + slideChoice); ➏
    obj.setAttribute("y1", pos);
    obj.setAttribute("y2", pos);

    rgb[slideChoice] = 100-pos; ➐

    var colorStr = "rgb(" + rgb[0] + "%," +  ➑
      rgb[1] + "%," + rgb[2] + "%)";
    obj = document.getElementById("shirt");
    obj.style.setProperty("fill", colorStr, null);
  }
}

❶ 鼠标按下的时候会调用 startColorDrag(evt)。它首先停止当前正在进行的拖动(如果有的话),然后将当前滑块设置为点击的这个(0= 红色,1= 绿色,2= 蓝色)。

❷ 鼠标按键释放的时候会调用 endColorDrag(evt)。这个函数也可能被其他函数调用。它会将当前滑块索引设为 -1,表示当前没有滑块被拖动。这个函数不需要处理 evt 参数。

❸ 鼠标移动时会调用 doColorDrag(evt)。这个函数会使用 evt 参数的 target 属性(来获知被拖动的是哪个滑块)和 clientY 属性(来获知当前鼠标的位置)。

❹ 检查当前是否有滑块在滑动,以及滑动的滑块是当前滑块。

❺ 获取指示线对象和鼠标位置(相对颜色块顶部换算过的位置),将位置值转换到 0~100 的范围。

❻ 将指示线移到新的鼠标位置。

❼ 计算该滑块对应的新颜色值。

❽ 以 rgb() 的形式写上颜色值以改变 T 恤的颜色。

这个例子中只有一个小地方需要特别注意:文档只会在鼠标指针在滑块内时响应 onmouseup 事件。所以如果你在红色的滑块上按下鼠标,然后拖动鼠标到 T 恤上,再释放鼠标,文档并不会注意到这个释放动作。当你把鼠标再次移回红色滑块时,它仍然会跟随鼠标。为了解决这个问题,我们添加了一个完全覆盖当前可视区域的透明矩形,然后让它来响应鼠标释放事件。当它响应 mouseup 事件时,会调用 stopColorDrag 方法。这个元素是第一个元素,这样它就会出现在图形的最底下。为了让这个元素不干扰图形,我们为它设置 style="fill: none;"。也许你会说:“等等,不是说好了透明元素不响应事件的吗?”通常情况下的确如此,但我们可以将 pointer-events 的值设为 visible,这样不管它的透明度是多少,只要没被隐藏就能响应事件。6

6pointer-events 的其他值包括只响应填充区域(fill)、只响应轮廓区域(stroke)以及响应填充和轮廓区域(painted),这些值都不管元素是否可见。与之对应的还有 visibleFillvisibleStrokevisiblePainted,也会考虑元素的可见性。

<rect id="eventCatcher" x="0" y="0" width="400" height="300"
  style="fill: none;" pointer-events="visible" />

修改 init() 函数中的事件监听部分:

document.getElementById("eventCatcher").
  addEventListener("mouseup", endColorDrag, false);

在第 2 章中,我们介绍过,在 HTML 文档中加入交互式的 SVG 有两种办法。如果只有少量 SVG,可以直接放入 HTML 中。如果有大量的 SVG 图形,可以通过 <object> 元素引用。下面的示例会展示如何将前面的例子放入 HTML 中:

<object id="externalShirt" data="shirt_interact.svg"
  type="image/svg+xml">
  <p>Alas, your browser does not support SVG.</p>
</object>

值得关注的是用于指定图形源(本例中是一个 URL,即一个相对路径)的 data 属性以及 type 属性,type 属性的值为 image/svg+xml。HTML 开启标记和结束标记之间的文字只会在图形无法加载时才显示出来。现在可以添加一些让 SVG 脚本和页面脚本交互的代码了,id 属性会在交互中扮演重要角色。

我们在 Web 页面中放置一个表单,让用户输入红色、绿色和蓝色的百分比。用户输入的值会反映到滑块上。如果用户拖动了滑块,那么表单中的值也会对应更新。

下面是 HTML 文档,其中引用了(还不存在的)updateSVG() 函数。这个函数会接受两个参数,分别是对应的滑块索引值和输入框中的值:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <title>SVG and HTML</title>
  <style type="text/css">
    /* 让表单项独占一行 */
    label {display: block;}
    h1 {font-size: 125%;}
  </style>
  <script type="text/javascript">
    /* 这里放置脚本代码 */
  </script>
</head>

<body>
<h1>SVG and HTML</h1>
<div style="text-align:center">
  <object id="shirt" data="shirt_interact.svg"
    type="image/svg+xml">
    <p>Alas, your browser cannot load this SVG file.</p>
  </object>

  <form id="rgbForm">
    <label>Red: <input id="fld0" type="text" size="5" value="100"
      onchange="updateSVG(0, this.value)" />% </label>
    <label>Green: <input id="fld1" type="text" size="5" value="100"
      onchange="updateSVG(1, this.value)" />% </label>
    <label>Blue: <input id="fld2" type="text" size="5" value="100"
      onchange="updateSVG(2, this.value)" />%</label>
  </form>
</div>
</body>
</html>

 为了避免演示代码过于复杂,我们没太注意编码风格,并且将脚本直接放入了 HTML 中。onchange="updateSVG(0, this.value)" 和添加一个 <input> 元素 change 事件监听器效果一样。当输入框的值变化时,执行 updateSVG(0, this.value)

updateHTMLField 函数将由 SVG 文档的脚本来调用。它接受两个参数,一个是输入框的索引,一个是值,值会显示在对应的输入框中:

function updateSVG(which, amount) {
  amount = parseInt(amount);
  if (!isNaN(amount) && window.setShirtColor) {
    window.setShirtColor(which, amount);
  }
}

function updateHTMLField(which, percent) {
  document.getElementById("fld" + which).value = percent;
}

接下来需要修改 SVG 文档。现在有两种方法修改 T 恤的颜色:一种是从滑块中取值,一种是从表单中取值。这样的话,我们首先需要将设置 T 恤颜色的代码从滑块拖动的代码中分离出来。修改后的 doColorDrag(evt) 函数会检测当前滑块并计算滑块的位置,但具体的修改会调用一个新方法 svgSetShirtColor

function doColorDrag(evt) {
  if (slideChoice >= 0) {
    var sliderId = evt.target.parentNode.getAttribute("id");
    chosen = parseInt(sliderId[sliderId.length - 1]);
    if (slideChoice == chosen) {
      svgSetShirtColor(slideChoice, 100 - (evt.clientY - 10));
    }
  }
}

svgSetShirtColor 函数会处理之前 doColorDrag 做的部分工作,但有两点不同:一是它会使用传入的滑块索引作为第一个参数,而不再使用全局变量 slideChoice;二是值也作为参数传入。类似这样的代码变化是在将一些临时的演示代码转变为更模块化的代码时必须做的。

function svgSetShirtColor(which, percent) {
  var obj;
  var colorStr;
  var newText;

  if (percent < 0) { percent = 0; } ➊
  if (percent > 100) { percent = 100; }

  obj = document.getElementById("slide" + which); ➋
  obj.setAttribute("y1", 100 - percent);
  obj.setAttribute("y2", 100 - percent);
  rgb[which] = percent;

  colorStr = "rgb(" + rgb[0] + "%," + ➌
    rgb[1] + "%," + rgb[2] + "%)";
  obj = document.getElementById("shirt");
  obj.style.setProperty("fill", colorStr, null);
}

❶ 需要检查值是否在正确的范围。

❷ 滑块移动到了新位置。

❸ 改变 T 恤颜色的代码和之前一样。

接下来,在 init 函数中使用 parent 来将 SVG 文档的 svgSetShirtColor 函数赋值给 HTML 页面的 setShirtColor 变量。之所以可以这样做,是因为当一个文档被嵌入到另一个文档中时,子文档的全局变量 parent 会指向父文档的 window 对象。因为 setShirtColor 会成为 Web 页面中 window 的属性,所以页面可以访问它。接下来的代码完成了 HTML 到 SVG 的通信。在使用 parent 变量前,先测试一下,以确保它存在(即确认 SVG 确实是被内嵌在另一个文档中)。

function init( evt ) {
  // 添加事件监听
  if (parent) {
        parent.setShirtColor = svgSetShirtColor;
    }
  transformShirt();
}

最后一步是完成 SVG 到 HTML 的通信,以便用户使用滑块调整颜色时也能正常工作。我们在用户拖拽结束时更新 HTML 表单,而不是一直不停地更新。在 endColorDrag 函数中添加下面加粗的代码。如果滑块被拖动了,会调用父页面中的 updateHTMLfield 函数(如果存在的话),并带上滑块的索引和值。

function endColorDrag( ) {

  if (slideChoice >= 0) {
    if (parent)
      parent.updateHTMLField(slideChoice, rgb[slideChoice]);
  }

  // 标记当前没有正被拖动的滑块
  slideChoice = -1;
}

结果如图 13-5 所示。为了节省屏幕空间,截图被编辑过。

{%}

图 13-5:HTML 与 SVG 交互的截图

你也可以查看线上示例:

http://oreillymedia.github.io/svg-essentials-examples/ch13/shirt_interact.html

除了可以修改已有元素的属性外,脚本也可以创建新元素。接下来我们为 T 恤这个例子添加一些飞镖盘状的圆环。结果如图 13-6 所示,我们确信这会是一件超级流行的衣服。

{%}

图 13-6:不同选项下的截图

你可以在在线示例中自己设计 T 恤:

http://oreillymedia.github.io/svg-essentials-examples/ch13/shirt_create.html

HTML 也需要做一些修改,添加一个新表单项来指定圆环的数量:

<label>Rings: <input id="nRings" type="text" size="3" value="0"
  onchange="createRings(this.value)" /></label>

SVG 图形在一个外部文档中,所以 HTML 中的脚本需要找到获取 SVG 图形的方法。在上一节中,SVG 文档中的脚本使用了 parent 来访问 HTML 脚本的环境。在这个示例中,HTML 中的脚本将使用 <object> 元素的 getSVGDocument() 方法来直接获取和修改 SVG DOM。SVG 文档和上一节完全一样。

为了访问到 SVG 文档,当页面加载完成时在 Web 页面的 <body> 上调用一个初始化方法:

<body onload="init()">

init() 函数会获取 SVG 文档并将它存在一个全局变量中:

var svgDoc;

function init() {
  var obj = document.getElementById("shirt");
  svgDoc = obj.getSVGDocument();
}

至此,HTML 页面脚本中的 createRings() 就能添加和移除 SVG 文档中的元素了,见示例 13-8。

示例 13-8:使用 JavaScript 创建元素

function createRings(nRings) {
  var shirt = svgDoc.getElementById("shirt"); ➊
  var rings = shirt.getElementsByTagName("circle"); ➋
  var i;
  var radius;
  var circle;

  for (i = rings.length - 1; i >= 0; i--) { ➌
    shirt.removeChild(rings[i]);
  }

  /* 限定范围为0-5 */
  if (nRings < 0) { nRings = 0; }
  else if (nRings > 5) { nRings = 5; }

  radius = nRings * 4;
  for (i = 0; i < nRings * 2; i++) {
    circle = svgDoc.createElementNS("http://www.w3.org/2000/svg",  ➍
      "circle");
    circle.setAttribute("cx", "0");
    circle.setAttribute("cy", "0");
    circle.setAttribute("r", radius);

    if (i % 2 == 0) { ➎
        circle.style.cssText = "fill:black; stroke:none";
    }
    else {
        circle.style.cssText = "fill:white; stroke:none;";
    }
    shirt.appendChild(circle); ➏
    radius -= 2;
  }
}

❶ 获取 SVG 文档中的 <g>

❷ 获取组中的所有 <circle> 元素。

❸ 通过调用 shirt 变量所指向的父元素 <g>removeChild(nodeToRemove),(逆序)移除所有的圆环。

❹ 要创建的元素属于 SVG 文档,所以必须在 SVG 的命名空间(NS)中创建。注意命名空间是使用 URL 定义的,而不是前缀。

➎ 为奇偶位置的圆环设置不同的样式。因为我们是在为一个新的没有样式的元素设置多个样式,所以直接使用 element.style.cssText 设置。

➏ 最后,使用 shirt 分组的 addChild(newNode) 将新创建的 <circle> 放入文档中并减小圆角半径以便下个圆环使用。SVG 文档中的新节点会按照它们被添加的顺序排序,即从大到小。这样小圆环会出现在大圆环上方。

 

30:00