此主题紧接基本 SVG 动画主题,将介绍一些中级 SVG 动画技术。若要完全理解此主题中所述的概念,请计划花 1 小时左右的时间来学习。
注意 要查看本主题中包含的示例,必须使用一个支持 SVG 元素的浏览器,如 Windows Internet Explorer 9。
在基本 SVG 动画中,我们主要介绍了对象的旋转。在本主题中,我们主要介绍对象的平移(即空间运动)以及这类平移的最常见结果 - 碰撞。
为了研究对象平移和碰撞,我们首先介绍可能最简单的对象 - 圆形。以下示例将在屏幕上移动圆形:
活动链接: 示例 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 onload="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 – r
和 boxHeight – r
)。
通过使用上面的碰撞检测技术,下面的示例将演示球(即圆形)弹离壁的轨迹:
活动链接: 示例 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 onload="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
<!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 onload="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。请注意,因长度的缘故,没有显示该示例代码,而使用 Windows Internet Explorer 中的View source功能查看关联的代码。为了方便起见,下面显示了示例 4 的屏幕截图:
首先,我们创建一个对象,该对象表示四个常用矢量运算的泛型矢量和函数:
如果理解基本矢量运算,那么这些函数可以直接实现。为了更好地了解矢量及其关联运算,请参阅 Wikipedia 或 Wolfram 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 中描述过,现在剩下球与球碰撞。遗憾的是,关联的数学运算非常复杂。在高级别上,要确定已碰撞两个球在碰撞后的正确速度矢量,需要进行下面的数学计算:
var Vab = vDiff(ballA.v, ballB.v);
var n = collisionN(ballA, ballB);
f = f_numerator / f_denominator;
ballA.v = vAdd( ballA.v, vMulti(f/Ma, n) ); ballB.v = vDiff( ballB.v, vMulti(f/Mb, n) );
有关详细信息,请参阅碰撞响应中的“Have Collision, Will Travel”部分。
活动链接: 示例 5
现在,我们已经介绍了球与壁及球与球的碰撞,我们可以延伸示例 4,将许多球全都放到一个球形竞技场(而不是盒中)中进行碰撞,一个“球竞技场”。
同样因为长度的缘故,没有显示该示例的代码(使用“查看源”可以查看这些代码)。但是提供了下面的屏幕截图:
要提到的关键代码相关项包括:
circle
元素),并将自定义属性追加到这些元素(如速度矢量对象)。 constants.epsilon
),可以调整每次弹跳所丢失的能量。值 1 指示不应丢失能量,与纯弹性碰撞一样。 示例 6 与示例 5 完全相同,但是它采用了更加面向对象的方式。
相对于最后两个示例,接下来的逻辑步骤可能包含:
这些扩展留作读者练习之用,应该会在很大程度上帮助你理解本主题中介绍的技术。