基本 SVG 动画

本主题将介绍基本 SVG 动画,它是学习中级 SVG 动画之前必须掌握的内容。此主题假定你基本了解 HTML 和 JavaScript。若要完全理解此主题所述的内容,请计划花 1 小时左右的时间来学习。

注意 要查看本主题中包含的示例,必须使用一个支持 SVG 元素的浏览器,如 Windows Internet Explorer 9。

尽管在 Internet Explorer 9 中不支持基本动画(如下所述),但在使用 SVG 的声明性动画结构 (SMIL) 时基本动画非常简单。例如,以下代码会以五秒的时间为间隔将一个方块旋转 90 度:

示例 1 - 基本的声明性 (SMIL) 动画

活动链接:示例 1(请注意,如本主题稍后所述,此特定示例无法在 Internet Explorer 9 中正常运行)

XML
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg width="800px" height="800px" viewBox="0 0 800 800"
     version="1.1" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"> <!-- Note that this is required in order to use xlink in the <use> element. -->

  <!-- THIS EXAMPLE NOT SUPPORTED IN INTERNET EXPLORER 9 -->
  
  <title>Simplest SVG Animation</title>
  <desc>SVG declarative animation is used to rotate a square.</desc>

  <g transform="translate(400, 400)"> <!-- Create a Cartesian coordinate system (with the y-axis flipped) for the animated square. That is, place the origin at the center of the 800 x 800 SVG viewport. --> 
  
    <!-- A 200 x 200 square with the upper left-hand corner at (-100, -100). This places the center of the square 
    at the origin (0, 0): -->  
    <rect x="-100" y="-100" width="200" height="200" rx="5" ry="5" 
          style=" fill: orange; stroke: black; stroke-width: 3; stroke-dasharray: 10, 5;">
      <animateTransform 
        attributeType="xml"
	    attributeName="transform" type="rotate"
	    from="0" to="90"
	    begin="0" dur="5s" 
	    fill="freeze"
      />
    </rect>
    
    <line x1="-400" y1="0" x2="400" y2="0" style="stroke: black;" /> <!-- Represents the x-axis. -->
    
    <line x1="0" y1="-400" x2="0" y2="400" style="stroke: black;" /> <!-- Represents the y-axis (although up is negative and down is positive). -->  
        
  </g>
</svg> 

上面的示例(以及所有下面的示例)带有相应的注释。但是,还有一些有帮助的要点需要指出,包括:

  • animateTransform 元素是我们要为其制作动画的对象(即 rect)的子对象,它将执行所有费力的工作,并且相对来说是不言自明的。

  • 因为方块的中心与视区的原点重合(坐标为 (400, 400)),所以方块将绕其中心旋转 90 度。举例来说,如果方块被定义为 x=“0”且 y=”0”,如下面所示:

    <rect x="0" y="0" width="200" height="200" rx="5" ry="5" style=" fill: orange; stroke: black; stroke-width: 3; stroke-dasharray: 10, 5;">
    
    

    则该方块的左上角(与其中心相对)将绕原点旋转 90 度。试一下!

尽管声明性动画很简单,但它还可能受到限制。用 SVG 基础的作者 David Eisenberg 的话说:"如果选择使用脚本执行动画,那么你将拥有脚本提供的所有交互功能;你可让动画依赖于鼠标位置或涉及多个变量的复杂条件。"

也就是说,利用基于脚本的动画,无论是简单还是复杂的动画,你都可能可以实现。因为这一点以及其他原因(如 CSS 动画),IE9 不支持声明性动画。不可否认的是,对于基于脚本的动画,你可能还有更多相关的工作要做,但是一旦掌握了这些脚本技术,你就可以实现使用纯声明性动画技术不可能实现的动画。以下示例(示例 1 的 JavaScript 版本(用 HTML5 编写))演示了这些技术中的部分技术:

示例 2 - 基本 JavaScript 动画

活动链接: 示例 2

<!DOCTYPE html>
<html>

<head>  
  <title>JavaScript SVG Animation</title>
  <!--  <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> Remove this comment only if you have issues rendering this page on an intranet site. -->
  <style>
    /* CSS here. */
  </style> 
  <script>  
    /* CONSTANTS */
    var initialTheta = 0; // The initial rotation angle, in degrees.
    var thetaDelta = 0.3; // The amount to rotate the square every "delay" milliseconds, in degrees.
    var delay = 10; // The delay between animation stills, in milliseconds. Affects animation smoothness.
    var angularLimit = 90; // The maximum number of degrees to rotate the square.
    
    /* 
      Note that it will take the square (angularLimit/thetaDelta)*delay milliseconds to rotate an angularLimit
      number of degrees. For example, (90/0.3)*10 = 3000 ms (or 3 seconds) to rotate the square 90 degrees.
    */
    
    /* GLOBALS */
    var theSquare; // Will contain a reference to the square element, as well as other things.
    var timer; // Contains the setInterval() object, used to stop the animation.
    
    function init()
    /*
      Assumes that this function is called after the page loads.
    */
    {
      theSquare = document.getElementById("mySquare"); // Set this custom property after the page loads.
      theSquare.currentTheta = initialTheta; // The initial rotation angle to use when the animation starts, stored in 
      timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.     
    }
    
    function doAnim()
    /*
      This function is called by setInterval() every "delay" milliseconds.
    */
    { 
      if (theSquare.currentTheta > angularLimit)
      {
        clearInterval(timer); // The square has rotated enough, instruct the browser to stop calling the doAnim() function.
        return; // No point in continuing; stop now.
      }
      
      theSquare.setAttribute("transform", "rotate(" + theSquare.currentTheta + ")"); // Rotate the square by a small amount.
      theSquare.currentTheta += thetaDelta;  // Increase the angle that the square will be rotated to, by a small amount.
    }
  </script>  
</head>

<body οnlοad="init(); doAnim();"> <!-- init() sets up the animation, doAnim() actually does the animation. -->
  <svg width="800px" height="800px" viewBox="0 0 800 800">
    <g transform="translate(400, 400)"> <!-- Create a Cartesian coordinate system (with the y-axis flipped) for the animated square. That is, place the origin at the center of the 800 x 800 SVG viewport: -->
  
      <!-- A 200 x 200 square with the upper left-hand corner at (-100, -100). This places the center of the square 
      at the origin (0, 0): -->  
      <rect id="mySquare" x="-100" y="-100" width="200" height="200" rx="5" ry="5" 
            style=" fill: orange; stroke: black; stroke-width: 3; stroke-dasharray: 10, 5;" />
            
      <!-- Represents the x-axis: -->
      <line x1="-400" y1="0" x2="400" y2="0" style="stroke: black;" /> 
    
      <!-- Represents the y-axis (although up is negative and down is positive): -->  
      <line x1="0" y1="-400" x2="0" y2="400" style="stroke: black;" /> 
                
    </g>
  </svg>
</body>

</html>

要点 与在 <head> 块中包括 <meta http-equiv-"X-UA-Compatible" content="IE-9" /><meta http-equiv-"X-UA-Compatible" content="IE-Edge" /> 相反,你可以使用 IE=Edge 将 Web 开发服务器配置为发送 X-UA-Compatible HTTP 标头,从而确保你在最新的标准模式中运行(如果在 Intranet 上进行开发)。

如果你了解传统“卡通”动画的基本原理,那么基于脚本的动画实际上就相对简单了。你可能知道,动画只不过是一系列静止的图像,其中的每个图像不断变化,它们会一个接一个快速地连续显示:

动画静态图像

如果这六个图像足够快地连续显示,人眼将会看到一个跳动的球:

跳动的球动画 GIF 文件

在此例中,跳动的球动画通过重复显示六个图像(每个图像会显示 100 毫秒,然后移动到下一个图像)产生。基于脚本的动画使用了相同的概念。在示例 2 的代码中,我们只调用了一个函数,该函数会不断地每隔几毫秒更改一个图像。具体而言,我们使用setInterval 来告诉浏览器每隔 delay 毫秒调用一次函数 doAnim。每次调用doAnim 函数时,它都会将方块旋转一小段弧度。因为 doAnim 每隔几毫秒就调用一次,所以方块看起来一直在旋转。当方块旋转angularLimit 数量的角度(示例中为 90°)后,我们会通过清除计时器变量来告诉浏览器停止调用 doAnim,然后动画就会停止(有关详细信息,请参见代码注释)。

在介绍更复杂的示例前,有一个重要的注意事项需要指出,那就是 JavaScript 编码的两种样式:

  • DOM L2 脚本和
  • SVG DOM 脚本

DOM L2 脚本属于“传统”的脚本,其特征是通过构建“值字符串”来设置各个项,如下所示:

theSquare.setAttribute("transform", "rotate(" + theSquare.currentTheta + ")");

SVG DOM 脚本的特征是缺少这种“值字符串”,通常以数字形式设置元素值,如下所述:

mySquare.transform.baseVal.getItem(0).setRotate(mySquare.currentTheta, 0, 0);

这两种方法的效果完全相同。唯一的细微差别是,setRotate 方法需要两个值来指示对象旋转的中心点,本例中的中心点为 (0, 0)。SVG DOM 脚本样式的优点是,你不必构建“值字符串”,而且性能优于 DOM L2 脚本。

下面的示例 3 是示例 2 的 SVG DOM 脚本版本:

示例 3 - SVG DOM 脚本

活动链接: 示例 3

<!DOCTYPE html>
<html>

<head>  
  <title>JavaScript SVG Animation</title>
  <!--  <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> Remove this comment only if you have issues rendering this page on an intranet site. -->
  <style>
    /* CSS here. */
  </style> 
  <script>  
    /* CONSTANTS */
    var initialTheta = 0; // The initial rotation angle, in degrees.
    var thetaDelta = 0.3; // The amount to rotate the square every "delay" milliseconds, in degrees.
    var delay = 10; // The delay between animation stills, in milliseconds. Affects animation smoothness.
    var angularLimit = 90; // The maximum number of degrees to rotate the square.
    
    /* 
      Note that it will take the square (angularLimit/thetaDelta)*delay milliseconds to rotate an angularLimit
      number of degrees. For example, (90/0.3)*10 = 3000 ms (or 3 seconds) to rotate the square 90 degrees.
    */
    
    /* GLOBALS */
    var timer; // Contains the setInterval() object, used to stop the animation.
    
    function init()
    /*
      Assumes that this function is called after the page loads.
    */
    {
      var transformObject;
      
      mySquare.currentTheta = initialTheta; // The initial rotation angle to use when the animation starts.
      timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.     
      transformObject = svgElement.createSVGTransform(); // Create a generic SVG transform object so as to gain access to its methods and properties, such as setRotate().
      mySquare.transform.baseVal.appendItem(transformObject); // Append the transform object to the square object, now the square object has inherited all the transform object's goodness.
    }
    
    function doAnim()
    /*
      This function is called by setInterval() every "delay" milliseconds.
    */
    { 
      var transformObject;
      
      if (mySquare.currentTheta > angularLimit)
      {
        clearInterval(timer); // Instruct the browser to stop calling the function indicated by setInterver();
        return;
      }
      
      mySquare.transform.baseVal.getItem(0).setRotate(mySquare.currentTheta, 0, 0); // Access the transform object (that was appended to mySquare in the init() function) and use its setRotate method to rotate the square about the point (0, 0) (which is at the center of the SVG viewport).
      mySquare.currentTheta += thetaDelta; // Place this line here so that the square isn't over rotated on the last call to doAnim().
    }
  </script>  
</head>

<body οnlοad="init(); doAnim();"> <!-- init() sets up the animation, doAnim() actually does the animation. -->
  <svg id="svgElement" width="800px" height="800px" viewBox="0 0 800 800"> <!-- Give the svg element a name so that we can easily access it via JavaScript. -->

    <g transform="translate(400, 400)"> <!-- Create a Cartesian coordinate system (with the y-axis flipped) for the animated square. That is, place the origin at the center of the 800 x 800 SVG viewport: -->
  
      <!-- A 200 x 200 square with the upper left-hand corner at (-100, -100). This places the center of the square 
      at the origin (0, 0). Give the square a name so we can easily access it via JavaScript: -->  
      <rect id="mySquare" x="-100" y="-100" width="200" height="200" rx="5" ry="5"
            style=" fill: orange; stroke: black; stroke-width: 3; stroke-dasharray: 10, 5;" />
            
      <!-- Represents the x-axis: -->
      <line x1="-400" y1="0" x2="400" y2="0" style="stroke: black;" /> 
    
      <!-- Represents the y-axis (although up is negative and down is positive): -->  
      <line x1="0" y1="-400" x2="0" y2="400" style="stroke: black;" /> 
                
    </g>
  </svg>
</body>

</html>

此代码(在 init 函数中)最难的部分可能是以下两行:

transformObject = svgElement.createSVGTransform(); 
mySquare.transform.baseVal.appendItem(transformObject);

第一行创建一个一般的变换对象。第二行将此变换对象附加到 mySquare 节点下的 DOM。这样,mySquare 对象便可以继承与某个变换对象关联的所有方法和属性。具体而言就是setRotate 方法。完成此操作后,我们只需调用 mySquare 方块的(新获取的)setRotate 方法就可以旋转它,如下所示:

mySquare.transform.baseVal.getItem(0).setRotate(mySquare.currentTheta, 0, 0);

此外,你还可以通过向 rect 元素添加“no-op”变换特性(如 transform="matrix(1 0 0 1 0 0)")来避免创建和附加变换对象:

<rect id="mySquare" x="-100" y="-100" width="200" height="200" rx="5" ry="5" 
          transform="matrix(1 0 0 1 0 0)"
          style=" fill: orange; stroke: black; stroke-width: 3; stroke-dasharray: 10, 5;" />

这将在矩形元素上创建一个变换对象,你随后可以对其进行操作而不必先“动态”创建和附加变换对象。完整的示例如下所示:

示例 4 - No-op 变换对象

活动链接: 示例 4

<!DOCTYPE html>
<html>

<head>  
  <title>JavaScript SVG Animation</title>
  <!--  <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> Remove this comment only if you have issues rendering this page on an intranet site. -->
  <style>
    /* CSS here. */
  </style>
  <script>  
    /* CONSTANTS */
    var initialTheta = 0; // The initial rotation angle, in degrees.
    var thetaDelta = 0.3; // The amount to rotate the square every "delay" milliseconds, in degrees.
    var delay = 10; // The delay between animation stills, in milliseconds. Affects animation smoothness.
    var angularLimit = 90; // The maximum number of degrees to rotate the square.
    
    /* 
      Note that it will take the square (angularLimit/thetaDelta)*delay milliseconds to rotate an angularLimit
      number of degrees. For example, (90/0.3)*10 = 3000 ms or 3 seconds to rotate the square 90 degrees.
    */
    
    /* GLOBALS */
    var timer; // Contains the setInterval() object, used to stop animation.
    
    function init()
    /*
      Assumes that this function is called after the page loads.
    */
    {
      mySquare.currentTheta = initialTheta; // The initial rotation angle to use when the animation starts.
      timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.     
    }
    
    function doAnim()
    /*
      This function is called by setInterval() every "delay" milliseconds.
    */
    { 
      if (mySquare.currentTheta > angularLimit)
      {
        clearInterval(timer);
        return;
      }
      
      mySquare.transform.baseVal.getItem(0).setRotate(mySquare.currentTheta, 0, 0); // Rotate the square about the point (0, 0), which is now at the center of the SVG viewport. Assumes a no-op transform attribute has been applied to the mySquare element, such as transform="matrix(1 0 0 1 0 0)".
      mySquare.currentTheta += thetaDelta; // Increase the angle that the square will be rotated to, by a small amount.      
    }
  </script>  
</head>

<body οnlοad="init(); doAnim();"> <!-- init() sets up the animation, doAnim() actually does the animation. -->
  <svg width="800px" height="800px" viewBox="0 0 800 800">

    <g transform="translate(400, 400)"> <!-- Create a Cartesian coordinate system (with the y-axis flipped) for the animated square. That is, place the origin at the center of the 800 x 800 SVG viewport: -->
  
      <!-- A 200 x 200 square with the upper left-hand corner at (-100, -100). This places the center of the square 
      at the origin (0, 0). Note that the no-op transform attribute is necessary to generate a transform object 
      such that the setRotate() method can be utilized in the doAnim() function: -->  
      <rect id="mySquare" x="-100" y="-100" width="200" height="200" rx="5" ry="5" 
            transform="matrix(1 0 0 1 0 0)"
            style=" fill: orange; stroke: black; stroke-width: 3; stroke-dasharray: 10, 5;" />

      <!-- Represents the x-axis: -->
      <line x1="-400" y1="0" x2="400" y2="0" style="stroke: black;" /> 
    
      <!-- Represents the y-axis (although up is negative and down is positive): -->  
      <line x1="0" y1="-400" x2="0" y2="400" style="stroke: black;" /> 
            
    </g>
  </svg>
</body>

</html>

示例 4 中,我们将单位矩阵用作“no-op”变换特性来生成一个变换对象。我们本来是可以轻松地改用transform=”rotate(0)” 的。

示例 3 较之示例 4 的一个优势是,你不需要惦记将“no-op”属性添加到矩形元素。

既然我们已经掌握了两种脚本样式,我们现在可以继续了解更有趣的动画。以下示例将尝试构建一个旋转的传动轮(本例中为摩擦轮)模型。假定有两个自行车车轮紧挨在一起旋转。橡胶的高摩擦系数将确保,当一个车轮旋转时,另一个车轮也会随之旋转。此处假定此圆形摩擦轮系统是理想化的:

两个摩擦轮

用于生成此图像的代码在下一个示例中显示:

示例 5 - 两个摩擦轮

活动链接: 示例 5

<!DOCTYPE html>
<html>

<head>  
  <title>Two Animated Gears</title>
  <!--  <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> Remove this comment only if you have issues rendering this page on an intranet site. -->
  <style>
    /* CSS here. */
  </style>  
  <script>  
    /* CONSTANTS */
    var initialTheta = 0; // The initial rotation angle, in degrees.
    var currentTheta = initialTheta; // The initial rotation angle to use when the animation starts.    
    var thetaDelta = 0.3; // The amount to rotate the gears every "delay" milliseconds, in degrees.
    var delay = 10; // The delay between animation stills, in milliseconds. Affects animation smoothness.
    var angularLimit = 360; // The maximum number of degrees to rotate the gears.
    
    /* 
      Note that it will take the gears (angularLimit/thetaDelta)*delay milliseconds to rotate an angularLimit
      number of degrees. For example, (90/0.3)*10 = 3000 ms (or 3 seconds) to rotate the gears 90 degrees.
    */
    
    /* GLOBALS */
    var timer; // Contains the setInterval() object, used to stop the animation.  
    
    function init()
    /*
      Assumes that this function is called after the page loads.
    */
    {
      var transformObject = svgElement.createSVGTransform(); // Create a generic SVG transform object so as to gain access to its methods and properties, such as setRotate().;
      
      gear0.transform.baseVal.appendItem(transformObject); // Append the transform object to gear0, now the gear0 object has inherited all the transform object's goodness.
      gear1.transform.baseVal.appendItem(transformObject); // Append the same generic transform object to gear1 - we just want gear1 to inherit all of it's goodness.    
    }
    
    function startAnim()
    {   
      if ( !startButton.startButtonClicked ) // Don't allow multiple instance of the function specified by setInterval to be invoked by the browser. Note that button.startButtonClicked will be undefined on first use, which is effectively the same as false.
      {   
        /* Only do the following once per animation: */
        timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.     
        startButton.startButtonClicked = true; // A custom property is attached to the button object to track whether the button has been clicked or not.  
      }
    }
    
    function doAnim()
    /*
      This function is called by setInterval() every "delay" milliseconds.
    */
    {       
      if (currentTheta > angularLimit)
      {
        clearInterval(timer); // Instruct the browser to stop calling the function indicated by setInterval();
        startButton.startButtonClicked = false; // Let the user run the animation again if they choose.
        currentTheta = initialTheta; // If we let the user run the animation multiple times, be sure to set currentTheta back to an appropriate value.
        return; // We have completed our animation, time to quit.
      }
      
      gear0.transform.baseVal.getItem(0).setRotate(currentTheta, -150, 0); // Rotate the 0th gear about the point (-150, 0).
      gear1.transform.baseVal.getItem(0).setRotate(-currentTheta, 150, 0); // Rotate the 1st gear, note the minus sign on currentTheta, this rotates the gear in the opposite direction.
      // gear0.setAttribute("transform", "rotate(" + currentTheta + ", -150, 0)"); // More cross-browser friendly, slightly less performant. Note that you don't technically need to append a transform object to each gear object, in init(), when using this line.
      // gear1.setAttribute("transform", "rotate(" + -currentTheta + ", 150, 0)"); // More cross-browser friendly, slightly less performant. Note that you don't technically need to append a transform object to each gear object, in init(), when using this line.
      currentTheta += thetaDelta; // Place this line here so that the gears are not over rotated on the last call to doAnim().
    }
  </script>  
</head>

<body οnlοad="init();"> <!-- init() sets up for the pending animation. -->
  <div align="center"> <!-- An inexpensive way to center everything. -->
    <div style=" margin-bottom: 8px;">
      <button id="startButton" type="button" οnclick="startAnim();">
        Start Animation
      </button> 
    </div> 
    <svg id="svgElement" width="800px" height="800px" viewBox="0 0 800 800"> <!-- Give the svg element a name so that we can easily access it via JavaScript. -->
      <rect x="0" y="0" width="100%" height="100%" rx="16" ry="16" 
            style="fill: none; stroke: black; stroke-dasharray: 10 5;" />
  
      <defs> <!-- Do not render the gear template, just define it. -->
        <g id="gearTemplate"> <!-- Give this group of graphic elements a name so that it can be "called" from the <use> element. -->
          <circle cx="0" cy="0" r="150" style="stroke: black;" />
          <line x1="0" y1="-150" x2="0" y2="150" style="stroke: white;"/> <!-- From top to bottom, draw the vertical wheel "spoke". -->        
          <line x1="-150" y1="0" x2="0" y2="0" style="stroke: white;"/> <!-- Draw left half of the horizontal "spoke". -->
          <line x1="0" y1="0" x2="150" y2="0" style="stroke: darkGreen;"/> <!-- Draw right half of the horizontal "spoke". -->
        </g>
      </defs>

      <g transform="translate(400, 400)"> <!-- Create a Cartesian coordinate system (with the y-axis flipped) for the animated gears. That is, place the origin at the center of the 800 x 800 SVG viewport: -->
        <use id="gear0" x="-150" y="0" xlink:href="#gearTemplate" style="fill: orange;" /> <!-- Use the previously defined gear template and position it appropriately. -->
        <use id="gear1" x="150" y="0" xlink:href="#gearTemplate" style="fill: mediumPurple;" /> <!-- Same as the previous line but give this circle a different color. -->                
      </g>
    </svg>
  </div>
</body>

</html>

示例 5 中,我们为两个图形对象创建了动画,并添加了“Start Animation”(启动动画)按钮,这导致相对于示例 4 需要重构很多代码。具体而言:

  • 我们使用 use 元素创建了一个可无限次使用的传动轮模板,而不是为两个“传动轮”各创建一个 SVG 标记。
  • 为了简化任务,我们将 currentTheta 设为全局变量,从而使它的值能应用于两个传动轮。
  • 引入了一个新函数 startAnim。现在,我们不必让加载页事件设置 setInterval 调用(通过使用init),而是可以在单击“Start Animation”(启动动画)按钮时完成此操作。
  • 如果用户多次单击“Start Animation”(启动动画)按钮,则会(没有保护)调用多个 doAnim 实例,从而使动画的运行速度超过预期。为停止此不需要的行为,我们将自定义属性startButtonClicked 附加到按钮对象,并在首次单击按钮时将其设置为 true
  • 为了让用户可以重新启动动画(在动画完成之后),我们向 doAnim 添加了两行代码:

    startButton.startButtonClicked = false; 
    currentTheta = initialTheta; 
    
    

    这两行代码仅在动画完成时(即,当 currentTheta 大于 angularLimit 时)执行。

示例 5 存在的问题之一是:对于每个传动轮,你必须单独调用其setRotate 方法,还必须记住每个传动轮的中心所在。如果你有很多这样的传动轮,那么这会很烦人。针对这个问题的一个解决方案是,使用一个传动轮数组,如示例 6 中所示(出于篇幅的原因,本文档中没有显示该示例代码。使用 Windows Internet Explorer 的“查看源文件”功能可以查看活动示例)。该示例的屏幕快照如下所示:

示例 6 - 17 个传动轮

活动链接: 示例 6

17 个非常酷的摩擦轮

示例 6 的代码中,你可以看到 17 个传动轮的动画效果。 我们并没有构建 17 个use 元素(与针对两个传动轮的示例 5 中一样,这些元素均具有独特的颜色和 (x, y) 值信息),而是以编程方式构建了一个传动轮数组,并在其中包含这些信息和其他有用的信息:

  • 每个传动轮的半径(这样,我们可以计算出传动轮的适当转速)。
  • 每个传动轮的当前角位置(度数)(即 currentAngle)。
  • 每个传动轮的名称等。

此外,我们使用传动轮数组来以编程方式将适当的元素附加到 DOM,从而使传动轮呈现在屏幕上。也就是说,JavaScript 代码会将传动轮元素附加到以下“coordinateFrame”g 元素:

<g id="coordinateFrame" transform="translate(400, 400)"> 
  <!-- Gear <g> elements will be appended here via JavaScript. -->
</g>

最大的传动轮(上面的“#0”)是主传动轮。主传动轮会旋转所有其余的传动轮(从动传动轮)。 因为只有一个主传动轮,所以我们将设置其转速 (constants.driveGearSpeed = 0.3),并根据它来计算其他传动轮所需的转速,如下所示:

gears[i].currentAngle += (gears[i].clockwise * constants.driveGearSpeed * (gears[constants.driveGearIndex].r/gears[i].r));

首先,每个传动轮通过其 gears[i].currentAngle 属性跟踪自己的角位置。传动轮应旋转的方向由 gears[i].clockwise(值为 1 或 -1)确定。给定的传动轮的当前角位置可以通过简单的计算得出:将主传动轮的角速度乘以主传动轮的半径,再除以当前传动轮的半径。请注意,对于主传动轮本身,gears[constants.driveGearIndex].r/gears[i].r 为 1,就是它应处的位置。

另外需要指出的是,示例中使用了自定义按钮属性 startButtonClicked,这使得按钮对象可以跟踪自己的当前状态(已单击或未单击)。

如果你有关于示例 6 的其他问题,请查看示例本身中的注释 – 对每个重要的行都进行了描述。

下一示例通过将单个“Start Animation”(启动动画)按钮替换为其他用户界面 (UI) 元素扩展了示例 6

示例 7 - 带有扩展的 UI 的 17 个传动轮

活动链接: 示例 7

本例通过添加以下 UI 按钮扩展了示例 6

添加的 UI 按钮的图像

“Start”(启动)、“Pause”(暂停)和“Reset”(重置)按钮用于启动、暂停和重置动画,而“+”和“-”按钮用于提高和降低动画的速度,这不足为奇。新 UI 导致必须更改很多代码,包括:

  • 不再有角度限制的必要性 – 我们让动画运行,直到用户单击“Pause”(暂停)或“Reset”(重置)按钮时为止。为了避免潜在的变量溢出,我们使用:
    if (gears[i].currentAngle >= 360)
      gears[i].currentAngle -= 360;
    
    

    这将使每个传动轮的 currentAngle 保持为较小的值,而不会影响 currentAngle 的含义。也就是说,旋转了 362 度的圆看起来与旋转了 2 度的圆(例如,362 – 360 = 2 度)一样。

示例 8 - 带有音频的 17 个传动轮

活动链接: 示例 8

利用 HTML5 audio 元素, 本示例可通过添加声音效果来扩展前面的示例,如果单击“+”或“-”按钮,声音效果的节奏将线性地加快或减慢。由于任何声音效果可能会让人厌烦,因此,我们还添加了方便用户的“Sound Off”(关闭声音)按钮:

添加的 UI 按钮的图像

示例 8 音频功能相对简单并且进行了很好的注释。唯一需要额外说明的功能是calculatePlaybackRate,该功能将根据主传动轮当前的转速返回相应的音频文件播放速度。为了帮助说明如何实现此功能,请考虑以下图像:

主传动轮与音频播放速度图形

x 轴表示主传动轮的当前转速(可能为正,也可能为负)。y 轴表示相应的音频文件播放速度(只可能为正)。我们知道,当主传动轮速度为 0 时,音频播放速度也应该为 0(即,没有声音)。从我们的初始化常量可以看出,当主传动轮速度为constants.initialDriveGearSpeed 时,音频播放速度为 constants.initialPlaybackRate。我们现在有两个点:(0,0) 和 (constants.initialDriveGearSpeed, constants.initialPlaybackRate)。因为两点决定一条直线(并且我们希望线性响应),所以通过计算直线的斜率 (m) 可以很容易得到calculatePlaybackRate 中使用的方程,并用它乘以当前传动轮速度的绝对值可获得正确的音频文件的播放速率(有关详细信息,请参见calculatePlaybackRate 中的注释)。

合乎逻辑的下一步骤应该是向示例 8 添加其他传动轮。 因为该示例的代码使用一个传动轮对象数组,所以用户只需向该数组中添加大小和位置合适的传动轮就能增加动画中的传动轮的总数。此任务留给读者作为练习,应该会在很大程度上帮助你理解本主题中介绍的技术。

相关主题

中级 SVG 动画 HTML5 图形 Scalable Vector Graphics (SVG)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值