中级 SVG 动画

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

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

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

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

示例 1 - 基本平移

活动链接: 示例 1

<!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 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)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值