中级 SVG 动画

此主题紧接基本 SVG 动画主题,将介绍一些中级 SVG 动画技术。若要彻底理解此主题中所述的概念,请计划花 1 小时左右的时间来学习。html

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

基本 SVG 动画中,咱们主要介绍了对象的旋转。在本主题中,咱们主要介绍对象的平移(即空间运动)以及这类平移的最多见结果 - 碰撞。浏览器

为了研究对象平移和碰撞,咱们首先介绍可能最简单的对象 - 圆形。如下示例将在屏幕上移动圆形:服务器

示例 1 - 基本平移

活动连接: 示例 1ide

<!DOCTYPE html>
<html>

<head>  
  <title>SVG Animation - Circle Translation</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>  
    var timer; // Contains the setInterval() object, which is used to stop the animation.
    var delay = 16; // Invoke the function specified in setInterval() every "delay" milliseconds. This value affects animation smoothness.
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function s2d(s)
    /* 
      The function name "s2d" means "speed to displacement". This function returns the required 
      displacement value for an object traveling at "s" pixels per second. This function assumes the following:
      
         * The parameter s is in pixels per second.
         * "constants.delay" is a valid global constant.
         * The SVG viewport is set up such that 1 user unit equals 1 pixel.      
    */    
    {     
      return (s / 1000) * delay; // Given "constants.delay", return the object's displacement such that it will travel at s pixels per second across the screen.
    }    

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function init()
    {
      svgElement = document.getElementById("svgElement"); // Required for Mozilla, this line is not necessary for IE9 or Chrome.    
      circle0 = document.getElementById("circle0"); // Required for Mozilla, this line is not necessary IE9 or Chrome.

      timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.
      
      /* Create custom properties to store the circle's velocity: */
      circle0.vx = 150; // Move the circle at a velocity of 50 pixels per second in the x-direction.
      circle0.vy = 80; // Move the circle at a velocity of 20 pixels per second in the y-direction.
    }  

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function doAnim()
    {      
      var r = circle0.r.baseVal.value; // The radius of circle0.
      var boxWidth = svgElement.width.baseVal.value; // The width of the SVG viewport.
      var boxHeight = svgElement.height.baseVal.value; // The height of the SVG viewport.
      
      circle0.cx.baseVal.value += s2d(circle0.vx); // Move the circle in the x-direction by a small amount.
      circle0.cy.baseVal.value += s2d(circle0.vy); // Move the circle in the y-direction by a small amount.
      
      if ( (circle0.cx.baseVal.value >= (boxWidth - r)) || (circle0.cy.baseVal.value >= (boxHeight - r)) ) // Detect if the circle attempts to exit the SVG viewport assuming the ball is moving to the right and down.
        clearInterval(timer); // The circle has hit the bottom or right wall so instruct the browser to stop calling doAnim().
    }
  </script>  
</head>

<body οnlοad="init();">
  <svg id="svgElement" width="800px" height="600px" viewBox="0 0 800 600">
    <rect x="0" y="0" width="100%" height="100%" rx="10" ry="10" style="fill: white; stroke: black;" />
    <circle id="circle0" cx="40" cy="40" r="40" style="fill: orange; stroke: black; stroke-width: 1;" />
  </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 上进行开发的话)。svg

如以上代码示例中所示,咱们使用 SVG DOM 脚本样式(有关对此样式的讨论,请参阅基本 SVG 动画)。函数

基本概念很是简单 – 每隔 16 毫秒(即,delay 的值),咱们将圆心位置移动一点。例如,在伪代码中,咱们使用:学习

<x-coordinate of circle> = <x-coordinate of circle> + 0.5
<y-coordinate of circle> = <y-coordinate of circle> + 0.2

咱们没有对 Δx 的值(即 0.5)和 Δy 的值(即 0.2)进行硬编码,而是经过向圆形元素追加两个新的自定义属性,为圆形指定了一个速度矢量:测试

circle0.vx = 50; // Move the circle at a velocity of 50 pixels per second in the x-direction.
circle0.vy = 20; // Move the circle at a velocity of 20 pixels per second in the y-direction.

能够经过图形方式表示此速度矢量 v,以下所示:动画

二维速度矢量

Figure 1

所以,circle0.vx 是圆形的速度矢量的 x 轴份量(单位为每秒像素数),而 circle0.vy 是速度矢量的 y 轴份量(单位为每秒像素数)。请注意,上面的 xy 坐标系表示原点在屏幕左上角的 SVG 视区。

咱们如今须要一个函数,将速度矢量的一个份量平移到相应的位移以实现动画目的。可经过使用 s2d(v) 函数来完成此操做。例如,若是 v 参数为每秒 50 个像素,且 delay 为 16 毫秒,则经过使用维度分析,获得的位移结果为 (50pixels/s)•(1s/1000ms)•(16ms) = 0.8 像素。

最终,当圆形碰到 SVG 视区的右侧或底部“框壁”时,动画中止。也就是说,咱们须要一个简单形式的碰撞检测:

if ( (circle0.cx.baseVal.value > (boxWidth - r)) || (circle0.cy.baseVal.value > (boxHeight - r)) )
  clearInterval(timer);

由于咱们须要肯定圆形的边什么时候碰到壁(相对于圆心),因此咱们必须减去圆的半径,如上面的代码段中所示(即 boxWidth – rboxHeight – r)。

经过使用上面的碰撞检测技术,下面的示例将演示球(即圆形)弹离壁的轨迹:

示例 2 - 一面壁弹跳

活动连接: 示例 2

<!DOCTYPE html>
<html>

<head>  
  <title>SVG Animation - Circle Translation</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>  
    var timer; // Contains the setInterval() object, used to stop the animation.
    var delay = 10; // Invoke the function specified in setInterval() every "delay" milliseconds. This value affects animation smoothness.
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function s2d(s)
    /* 
      The function name "s2d" means "speed to displacement". This function returns the required 
      displacement value for an object traveling at "s" pixels per second. This function assumes the following:
      
         * The parameter s is in pixels per second.
         * "constants.delay" is a valid global constant.
         * The SVG viewport is set up such that 1 user unit equals 1 pixel.      
    */    
    {     
      return (s / 1000) * delay; // Given "constants.delay", return the object's displacement such that it will travel at s pixels per second across the screen.
    }    

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function init()
    {
      svgElement = document.getElementById("svgElement"); // Required for Mozilla, this line is not necessary IE9 or Chrome.    
      circle0 = document.getElementById("circle0"); // Required for Mozilla, this line is not necessaryIE9 or Chrome.
    
      timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.
      
      /* Create custom properties to store the circle's velocity: */
      circle0.vx = 150; // Move the circle at a velocity of 50 pixels per second in the x-direction.
      circle0.vy = 60; // Move the circle at a velocity of 20 pixels per second in the y-direction.
    }  
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function doAnim()
    {
      var r = circle0.r.baseVal.value; // The radius of circle0.
      var boxWidth = svgElement.width.baseVal.value; // The width of the SVG viewport.
      var boxHeight = svgElement.height.baseVal.value; // The height of the SVG viewport.
      
      circle0.cx.baseVal.value += s2d(circle0.vx); // Move the circle in the x-direction by a small amount.
      circle0.cy.baseVal.value += s2d(circle0.vy); // Move the circle in the y-direction by a small amount.
      
      /* Assumes the circle's velocity is such that it will only hit the right wall: */
      if ( circle0.cx.baseVal.value >= (boxWidth - r) ) // Detect if the circle attempts to exit the right side of the SVG viewport.
        circle0.vx *= -1; // Reverse the direction of the x-component of the ball's velocity vector - this is a right-wall bounce.
      
      if ( circle0.cy.baseVal.value >= (boxHeight - r) )
        clearInterval(timer); // The circle has hit the bottom wall so instruct the browser to stop calling doAnim().
    }
  </script>  
</head>

<body οnlοad="init();">
  <svg id="svgElement" width="800px" height="600px" viewBox="0 0 800 600">
    <rect x="0" y="0" width="100%" height="100%" rx="10" ry="10" style="fill: white; stroke: black;" />
    <circle id="circle0" cx="40" cy="40" r="40" style="fill: orange; stroke: black; stroke-width: 1;" />
  </svg>
</body>

</html>

球弹离壁的关键概念是矢量反射,如如下简化图形所示:

脱离墙的矢量反射

Figure 2

在图 2 中,右侧黑色虚线表示壁,vin 表示球碰到壁以前的速度矢量,vout 表示球碰到壁以后的速度矢量。你能够看到(在此特定状况下),惟一变化的是向外速度矢量幅度的 x 轴份量的符号。所以,要使球弹离右壁,只需改变球的速度矢量的 x 轴份量的符号便可:

if ( circle0.cx.baseVal.value > (boxWidth - r) )
  circle0.vx *= -1; 

请注意,咱们已经决定在球碰到底壁时中止动画:

if ( circle0.cy.baseVal.value > (boxHeight - r) )
  clearInterval(timer);

上面的示例存在某种人为设定的因素,只有在球最初彻底按正确的方向移动时,代码才会起做用。接下来的示例消除了人为设定的因素。但在你继续以前,再看一下图 2。想像蓝色矢量弹离左壁。应该很明显,依照右壁的状况,你只须要更改速度矢量的 x 轴份量的符号便可得到正确的行为。经过对顶壁和底壁使用此相同参数,能够看出,你只须要更改 y 轴份量的符号便可得到正确的结果。这是在如下示例中使用的逻辑:

示例 3 - 四面壁弹跳

活动连接: 示例 3

<!DOCTYPE html>
<html>

<head>  
  <title>SVG Animation - Circle Translation</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>  
    var timer; // Contains the setInterval() object, used to stop the animation.
    var delay = 10; // Invoke the function specified in setInterval() every "delay" milliseconds. This value affects animation smoothness.
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function s2d(s)
    /* 
      The function name "s2d" means "speed to displacement". This function returns the required 
      displacement value for an object traveling at "s" pixels per second. This function assumes the following:
      
         * The parameter s is in pixels per second.
         * "constants.delay" is a valid global constant.
         * The SVG viewport is set up such that 1 user unit equals 1 pixel.      
    */    
    {     
      return (s / 1000) * delay; // Given "constants.delay", return the object's displacement such that it will travel at s pixels per second across the screen.
    }    

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function init()
    {
      svgElement = document.getElementById("svgElement"); // Required for Mozilla, this line is not necessary for IE9 or Chrome.    
      circle0 = document.getElementById("circle0"); // Required for Mozilla, this line is not necessary for IE9 or Chrome.
    
      timer = setInterval(doAnim, delay); // Call the doAnim() function every "delay" milliseconds until "timer" is cleared.
      
      /* Create custom properties to store the circle's velocity: */
      circle0.vx = 200; // Move the circle at a velocity of 200 pixels per second in the x-direction.
      circle0.vy = 80; // Move the circle at a velocity of 80 pixels per second in the y-direction.
    }  
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function verticalWallCollision(r, width)
    /* 
      Returns true if circl0 has hit (or gone past) the left or the right wall; false otherwise.
    */
    {
      return ( (circle0.cx.baseVal.value <= r) || (circle0.cx.baseVal.value >= (width - r)) );
    }
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function horizontalWallCollision(r, height)
    /* 
      Returns true if circl0 has hit (or gone past) the top or the bottom wall; false otherwise.
    */
    {
      return ( (circle0.cy.baseVal.value <= r) || (circle0.cy.baseVal.value >= (height - r)) );
    }    
    
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */    
    
    function doAnim()
    {
      var r = circle0.r.baseVal.value; // The radius of circle0.
      var boxWidth = svgElement.width.baseVal.value; // The width of the SVG viewport.
      var boxHeight = svgElement.height.baseVal.value; // The height of the SVG viewport.
      
      circle0.cx.baseVal.value += s2d(circle0.vx); // Move the circle in the x-direction by a small amount.
      circle0.cy.baseVal.value += s2d(circle0.vy); // Move the circle in the y-direction by a small amount.
      
      if ( verticalWallCollision(r, boxWidth) )
        circle0.vx *= -1; // Reverse the direction of the x-component of the ball's velocity vector.
      
      if ( horizontalWallCollision(r, boxHeight) )
        circle0.vy *= -1; // Reverse the direction of the y-component of the ball's velocity vector.
    }
  </script>  
</head>

<body οnlοad="init();">
  <svg id="svgElement" width="800px" height="600px" viewBox="0 0 800 600">
    <rect x="0" y="0" width="100%" height="100%" rx="10" ry="10" style="fill: white; stroke: black;" />
    <circle id="circle0" cx="40" cy="40" r="40" style="fill: orange; stroke: black; stroke-width: 1;" />
  </svg>
</body>

</html>

示例 2 - 一面壁弹跳示例 3 - 四面壁弹跳之间惟一显著的区别在于 verticalWallCollision(r, width)horizontalWallCollision(r, height) 这两个函数。后一个函数仅包含下面一行代码:

return ( (circle0.cy.baseVal.value <= r) || (circle0.cy.baseVal.value >= (height - r)) );

使用下图能够轻松理解这行彷佛很神秘的代码:

水平壁碰撞

Figure 3

如图 3 中所示,当球心的 y 坐标大于或等于相对于底壁 r 的距离时,表示球已经与底壁碰撞。此距离简单表示为 height – r。所以,咱们对底壁的测试将变成:

circle0.cy.baseVal.value >= (height - r)

一样,当球心的 y 坐标小于或等于距离 r 时,表示球已经与顶壁碰撞。再次,此距离简单表示为 r – 0 = r,所以对顶壁的测试为:

circle0.cy.baseVal.value <= r

合并这两个测试将产生上面的返回语句。

示例 4 - 两球碰撞

活动连接: 示例 4

观看一个球在盒子中来回弹跳能够娱乐几分钟时间。不过,下一步向盒子中添加另外一个球后,会增添一些乐趣。执行此操做要求处理球与球碰撞以及相关数学运算。 为了帮助你开始操做,下面提供了示例 4。请注意,因长度的缘故,没有显示该示例代码,而使用 Windows Internet Explorer 中的View source功能查看关联的代码。为了方便起见,下面显示了示例 4 的屏幕截图:

示例 4 的屏幕截图

首先,咱们建立一个对象,该对象表示四个经常使用矢量运算的泛型矢量和函数:

  • 矢量加。
  • 矢量减。
  • 标量与矢量相乘。
  • 两个矢量的点积(标量)。

若是理解基本矢量运算,那么这些函数能够直接实现。为了更好地了解矢量及其关联运算,请参阅 WikipediaWolfram MathWorld

请注意,在该示例中,矢量函数包含在标记为“VECTOR FUNCTIONS”的脚本块内,且带有相应的注释。可是,关于这方面要指出的一点是,每一个圆形元素(即球)按以下所示沿本身的速度矢量运动(请参阅 init 函数):

var gv0 = new Vector(0, 0);

ball0.v = gv0;
ball0.v.xc = 200;      
ball0.v.yc = 80;

在上面,本地建立了一个新的泛型矢量 gv0,且该矢量追加到全局 ball0 圆形元素。完成此操做后,球 0 的速度矢量的 x 轴向份量和 y 轴向份量将分别设置为每秒 200 像素和每秒 80 像素。

球与壁碰撞已在示例 3 中描述过,如今剩下球与球碰撞。遗憾的是,关联的数学运算很是复杂。在高级别上,要肯定已碰撞两个球在碰撞后的正确速度矢量,须要进行下面的数学计算:

  1. 使用两个球碰撞前的速度矢量计算相对速度 Vab
    var Vab = vDiff(ballA.v, ballB.v);
    
    
  2. 计算碰撞点的法向单位矢量 n
    var n = collisionN(ballA, ballB);
    
    
  3. 计算“冲量”f 使得动量保持守恒:
    f = f_numerator / f_denominator;
    
    
  4. 使用两个球碰撞前的速度矢量计算相对速度 Vab
    ballA.v = vAdd( ballA.v, vMulti(f/Ma, n) ); 
    ballB.v = vDiff( ballB.v, vMulti(f/Mb, n) );
    
    

有关详细信息,请参阅碰撞响应中的“Have Collision, Will Travel”部分。

示例 5 - 所有放在一块儿:球竞技场

活动连接: 示例 5

如今,咱们已经介绍了球与壁及球与球的碰撞,咱们能够延伸示例 4,将许多球全都放到一个球形竞技场(而不是盒中)中进行碰撞,一个“球竞技场”。

一样由于长度的缘故,没有显示该示例的代码(使用“查看源”能够查看这些代码)。可是提供了下面的屏幕截图:

球竞技场的屏幕截图

要提到的关键代码相关项包括:

  • 以编程方式建立全部球元素(即 circle 元素),并将自定义属性追加到这些元素(如速度矢量对象)。
  • 每一个球的颜色、半径和初始位置(在竞技场内)是随机选择的,所以每次刷新该页面后都会得到不一样的初始条件集。
  • 由于各个球再也不弹离简单盒的壁,因此常规矢量反射的等式 v – 2(v•n)n 用来计算球在碰撞竞技场壁后的正确速度矢量。有关详细信息,请参阅 Wofram MathWorld 中的 反射
  • 每一个球的质量等于球(即圆)的面积。
  • 经过调整恢复系数(即 constants.epsilon),能够调整每次弹跳所丢失的能量。值 1 指示不该丢失能量,与纯弹性碰撞同样。

示例 6 - 面向对象的球竞技场

示例 6示例 5 彻底相同,可是它采用了更加面向对象的方式。

练习

相对于最后两个示例,接下来的逻辑步骤可能包含:

  • 添加剧置按钮。
  • 添加按钮以增长和减小在模拟中使用的球数目。
  • 添加按钮以增大和减少模拟速度。
  • 添加按钮以减少恢复系数。
  • 添加用于切换球线跟踪的按钮(每一个球都会留下一条“移动轨迹”,这指示球中心已经通过的位置)。
  • 最重要的是,提升模拟中使用的尽量简单的碰撞检测。

这些扩展留做读者练习之用,应该会在很大程度上帮助你理解本主题中介绍的技术。

相关主题

基本 SVG 动画
HTML5 图形
Scalable Vector Graphics (SVG)