Intermediate SVG Animation
This topic introduces intermediate level SVG animation techniques and starts where the Basic SVG Animation topic left off. To fully understand the concepts presented in this topic, plan to spend about 1 hour of study.
Note In order to view the examples contained within the topic, you must use a browser, such as Windows Internet Explorer 9 and later, that supports the SVG element.
- Example 1 - Basic Translation
- Example 2 - One Wall Bounce
- Example 3 - Four Wall Bounce
- Example 4 - Two Ball Collisions
- Example 5 - Putting it all together: Ball Arena
- Example 6 - Object Oriented Ball Arena
- Exercises
- Related topics
In Basic SVG Animation, we primarily focused on the rotation of objects. In this topic, we focus on translation (that is, spatial movement) of objects, and the most common result of such translations – collisions.
To investigate object translation and collision, we start with perhaps the simplest possible object – the circle. The following example moves a circle across the screen:
Example 1 - Basic Translation
<!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>
svg {
display: block; /* SVG is inherently an inline element. */
margin: 0 auto; /* Center the SVG viewport. */
}
</style>
</head>
<body>
<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>
<script>
"use strict";
var delay = 16; // Used to compute the required displacement value.
var svgElement = document.getElementById("svgElement");
var circle0 = document.getElementById("circle0");
/* Create custom properties to store the circle's velocity: */
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.
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.
var requestAnimationFrameID = window.requestAnimationFrame(doAnim); // Call the doAnim() function to start the demo.
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
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 doAnim() {
requestAnimationFrameID = window.requestAnimationFrame(doAnim); // Continue calling the doAnim() function until the circle has hit the bottom or right wall of 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.
window.cancelAnimationFrame(requestAnimationFrameID); // The circle has hit the bottom or right wall so instruct the browser to stop calling doAnim().
}
</script>
</body>
</html>
Important As opposed to including <meta http-equiv-"X-UA-Compatible" content="IE-Edge" />
within the <head>
block, you can configure your web development server to send the X-UA-Compatible HTTP header by using IE=Edge
to ensure that you are running in the latest standards mode, if you are developing on an intranet.
As you can see in the previous code example, we use the SVG DOM scripting style (see Basic SVG Animation for a discussion of this style).
The basic concept is simple – every 16 milliseconds (that is, the value of the delay
) we change the position of the circle’s center by a small amount. For example in pseudo-code, we have:
<x-coordinate of circle> = <x-coordinate of circle> + 0.5
<y-coordinate of circle> = <y-coordinate of circle> + 0.2
Instead of hardcoding values for Δx (i.e., 0.5) and Δy (i.e., 0.2), we specified a velocity vector for the circle by appending two new custom properties to the circle element:
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.
This velocity vector v can be graphically represented as follows:
Figure 1
Thus, circle0.vx
is the x-component of the circle’s velocity vector (in pixels per second) and circle0.vy
is the y-component of the vector (in pixels per second). Be aware that the above xy-coordinate system represents the SVG viewport, which has the origin in the upper-left corner of the screen.
We now need a function to translate one of these component velocity vectors into an appropriate displacement for animation purposes. This is accomplished by using the s2d(v)
function. For example, if the parameter v
is 50 pixels per second and delay
is 16 milliseconds, then by using dimensional analysis, we have (50pixels/s)•(1s/1000ms)•(16ms) = 0.8 pixels of displacement.
Lastly, we stop the animation when the circle has struck either the right or bottom "box wall" of the SVG viewport. That is, we need a simple form of collision detection:
if ( (circle0.cx.baseVal.value > (boxWidth - r)) || (circle0.cy.baseVal.value > (boxHeight - r)) )
clearInterval(timer);
Because we want to determine when the edge of the circle strikes a wall (as opposed to the circle’s center), we must subtract the radius of the circle, as shown in the above code snippet (that is, boxWidth – r
and boxHeight – r
).
By using the above collision detection technique, the following example shows how to bounce a ball (that is, a circle) off of a wall:
Example 2 - One Wall Bounce
<!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>
svg {
display: block; /* SVG is inherently an inline element. */
margin: 0 auto; /* Center the SVG viewport. */
}
</style>
</head>
<body>
<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>
<script>
"use strict";
var requestAnimationFrameID; // Contains the requestAnimationFrame() object, used to stop the animation.
var delay = 16; // Used to compute the required displacement value.
var svgElement = document.getElementById("svgElement"); // Required for Mozilla, this line is not necessary IE9 or Chrome.
var circle0 = document.getElementById("circle0"); // Required for Mozilla, this line is not necessaryIE9 or Chrome.
circle0.vx = 150; // Move the circle at a velocity of 150 pixels per second in the x-direction.
circle0.vy = 60; // Move the circle at a velocity of 60 pixels per second in the y-direction.
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.
requestAnimationFrameID = window.requestAnimationFrame(doAnim); // Call the doAnim() function.
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
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 doAnim() {
requestAnimationFrameID = window.requestAnimationFrame(doAnim); // Continue calling the doAnim() function until the circle has hit the bottom wall.
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))
window.cancelAnimationFrame(requestAnimationFrameID); // The circle has hit the bottom wall so instruct the browser to stop calling doAnim().
}
</script>
</body>
</html>
The key concept for bouncing a ball off a wall is vector reflection, as described in the following simplified graphic:
Figure 2
In Figure 2, the right black dashed line represents the wall, vin represents the velocity vector of the ball before it hits the wall, and vout represents the velocity vector of the ball after it hits the wall. As you can see (in this specific case), the only thing that changes is the sign of the x-component of the magnitude of the outgoing velocity vector. Thus, all that is required to bounce the ball off the right wall is a sign change on the x-component of the ball’s velocity vector:
if ( circle0.cx.baseVal.value > (boxWidth - r) )
circle0.vx *= -1;
Be aware that we have decided to stop the animation when the ball strikes the bottom wall:
if ( circle0.cy.baseVal.value > (boxHeight - r) )
clearInterval(timer);
The previous example is artificial in the sense that the code only works if the ball is initially moving in exactly the correct direction. The next example rectifies this artificiality. However, before you continue, take one more look at Figure 2. Imagine the blue vector bouncing off the left wall. It should be clear that, as is the case for the right wall, you only need to change the sign of the x-component of the velocity vector to obtain the correct behavior. By using this same argument for both the top and bottom walls, it becomes clear that you only need to change the sign of the y-component to obtain the correct result. This is the logic used in the following example:
Example 3 - Four Wall Bounce
<!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>
svg {
display: block; /* SVG is inherently an inline element. */
margin: 0 auto; /* Center the SVG viewport. */
}
</style>
</head>
<body>
<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>
<script>
"use strict";
var requestAnimationFrameID; // Contains the requestAnimationFrame() object.
var delay = 20; // Used to compute the required displacement value.
var svgElement = document.getElementById("svgElement"); // Required for Mozilla, this line is not necessary for IE9 or Chrome.
var circle0 = document.getElementById("circle0"); // Required for Mozilla, this line is not necessary for IE9 or Chrome.
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.
/* 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.
requestAnimationFrameID = window.requestAnimationFrame(doAnim); // Call the doAnim() function.
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
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 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() {
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.
requestAnimationFrameID = window.requestAnimationFrame(doAnim); // Continue calling the doAnim() function.
}
</script>
</body>
</html>
The only significant difference between Example 2 - One Wall Bounce and Example 3 - Four Wall Bounce are the two functions verticalWallCollision(r, width)
and horizontalWallCollision(r, height)
. The later function simply consists of the following line:
return ( (circle0.cy.baseVal.value <= r) || (circle0.cy.baseVal.value >= (height - r)) );
This quasi-cryptic line can be easily understood using the following figure:
Figure 3
As you can see from Figure 3, the ball has collided with the bottom wall when the y-coordinate of the ball’s center is greater than or equal to a distance r from the bottom wall. This distance is simply height – r. Thus, our test for the bottom wall becomes:
circle0.cy.baseVal.value >= (height - r)
Similarly, the ball has collided with the top wall when the y-coordinate of the ball’s center is less than or equal to a distance r. Again, this distance is simply r – 0 = r, so the test for the top wall is:
circle0.cy.baseVal.value <= r
Combining both tests results in the above return statement.
Example 4 - Two Ball Collisions
Watching one ball bounce around in a box is fun for a few minutes. However, the next step of adding another ball to the box can add some interest. Doing this requires dealing with ball-to-ball collisions and the mathematics thereof. To help get you started, here is Example 4. Note that because of its length, this example code is not shown – instead, use the View source feature in Windows Internet Explorer to view the associated code. For convenience, a screenshot of Example 4 follows:
To start, we create an object that represents a generic vector and functions for four common vector operations:
- Vector addition.
- Vector subtraction.
- The multiplication of a scalar with a vector.
- The dot product of two vectors (which is a scalar).
These functions are straightforward to implement if basic vector operations are understood. For a good review of vectors and their associated operations, see Wikipedia or Wolfram MathWorld.
Be aware that in this example, the vector functions are contained within the block of script marked “VECTOR FUNCTIONS” and are well commented. One item to point out in this regard, however, is the fact that each circle element (that is, a ball) tracks its own velocity vector as follows (see the init
function):
var gv0 = new Vector(0, 0);
ball0.v = gv0;
ball0.v.xc = 200;
ball0.v.yc = 80;
Here, a new generic vector gv0
is locally created and appended to the global ball0
circle element. After this is accomplished, the x-component and y-component of ball 0’s velocity vector are set to 200 and 80 pixels per second, respectively.
Ball-to-wall collisions were described in Example 3, which leaves ball-to-ball collisions. Unfortunately, the associated mathematics are nontrivial. At a high level, the following mathematical computations are necessary to determine the correct post-collision velocity vectors for two balls that have collided:
Using the two pre-collision velocity vectors for the balls, compute the relative velocity Vab:
var Vab = vDiff(ballA.v, ballB.v);
Calculate a unit vector n that is normal to the point of collision:
var n = collisionN(ballA, ballB);
Calculate the “impulse” f such that conservation of momentum is conserved:
f = f_numerator / f_denominator;
Using the two pre-collision velocity vectors for the balls, compute the relative velocity Vab:
ballA.v = vAdd( ballA.v, vMulti(f/Ma, n) ); ballB.v = vDiff( ballB.v, vMulti(f/Mb, n) );
For more information, see the "Have Collision, Will Travel" section of Collision Response.
Example 5 - Putting it all together: Ball Arena
Now that ball-to-wall and ball-to-ball collisions have been described, we can extend Example 4 to include many balls all colliding within a circular arena (as opposed to a box) - a "ball arena."
Again, because of its length, the code for this example is not shown (use View source to view the code). However, a screen shot follows:
Key code related items to mention include:
- All ball elements (that is,
circle
elements) are programmatically created, and custom properties are appended to these elements (such as a velocity vector object). - The color, radius, and initial position (within the arena) of each ball are randomly chosen, thus you get a different set of initial conditions each time the page is refreshed.
- Because the balls are no longer bouncing off of the walls of a simple box, the equation for general vector reflection, v – 2(v•n)n, is used to calculate the correct arena wall post-collision velocity vector for a ball. For more information, see Reflection at Wofram MathWorld.
- The mass of each ball is equal to the ball’s (that is, circle’s) area.
- You can adjust how much energy is lost per bounce by adjusting the coefficient of restitution (that is,
constants.epsilon
). A value of 1 indicates that no loss of energy should occur, as with purely elastic collisions.
Example 6 - Object Oriented Ball Arena
Example 6 is exactly the same as Example 5 except that it has been implemented in a much more object-oriented way.
Exercises
Relative to the last two examples, the next logical steps might include:
- Add a reset button.
- Add buttons to increase and decrease the number of balls used in the simulation.
- Add a buttons to increase and decrease the speed of the simulation.
- Add a button to decrease the coefficient of restitution.
- Add a button that toggles ball line tracing (in the sense that each ball leaves a “snail trail”, which indicates where its center has traveled).
- And most importantly, improve the simple-as-possible collision detection used in the simulation.
These extensions are left as an exercise to the reader and should significantly help your understanding of the techniques presented in this topic.