Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
Timers can be used to call a javascript function at set intervals. There are two ways to call a timer function:
A timer that has been set by setInterval() can be stopped by calling:
In order to use clearInterval(), a timer variable needs to be associated with a call to setInterval. This is done below:
let myTimer = setInterval(myFunction, 100) clearInterval(myTimer)
Use setTimeout() to set the start time of an animation. Use setInterval() to call a function that will control the actions of the animation.
The code below shows how to implement an animation interrupt.
let numberOfFrames = null let currentFrame = null function renderAnimation() { currentFrame = 0 animationInterval = setInterval(renderCanvas, 1000) // wait one second (1000 milli-seconds) } function renderCanvas() { // test to see if all of the frames have been played if(currentFrame === numberOfFrames)
{
clearInterval(animationInterval)
resetAnimation()
}
else // render the current frame and increment currentFrame { // render currentFrame ... // increment the currentFrame currentFrame++ }
}
The example below animates the drawing of a square as it moves across a canvas.
Example of an interval (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <script> let canvas = null let ctx = null let animationInterval = null // set to null when not running const STEP_SIZE = 1 const NUMBER_OF_FRAMES_PER_SECOND = 25 let frameRate = 1000 / NUMBER_OF_FRAMES_PER_SECOND let x = null window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // hide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight renderCanvas() startAnimationTimer() } function startAnimationTimer() { if (animationInterval === null) { x = 0 animationInterval = setInterval(renderCanvas, frameRate) } } function renderCanvas() { // clear any previous animation ctx.clearRect(0, 0, canvas.width, canvas.height) // test to see if all of the frames have been played if (x > (canvas.width + 1)) { clearInterval(animationInterval) animationInterval = null // set to null when not running } else // render the current frame and increment currentFrame { // render currentFrame ctx.fillRect(x, 100, 50, 50) // increment the currentFrame x += STEP_SIZE } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
The example above uses a timer to control the display of a black square moving across the canvas. When the square reaches the right side of the canvas, clearInterval() is called to remove the timer.
Adjust the code above, so that it has an image has a background, as shown here.
Adjust the above code, to show an image moving across another image, as shown here.
It is good coding practice to separate the rendering code from the state code. For the above example, this means that renderCanvas() should only deal with drawing.
Example where rendering and state code are separated (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <script> let canvas = null let ctx = null let animationInterval = null // set to null when not running const STEP_SIZE = 1 const NUMBER_OF_FRAMES_PER_SECOND = 25 let frameRate = 1000 / NUMBER_OF_FRAMES_PER_SECOND let x = null window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // hide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight renderCanvas() startAnimationTimer() } function renderCanvas() { // only include drawing code here ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.fillRect(x, 100, 50, 50) } function updateRectangleState() { // test to see if all of the frames have been played if (x > (canvas.width + 1)) { clearInterval(animationInterval) animationInterval = null // set to null when not running } else // render the current frame and increment currentFrame { // render currentFrame renderCanvas() // increment the currentFrame x += STEP_SIZE } } function startAnimationTimer() { if (animationInterval === null) { x = 0 animationInterval = setInterval(updateRectangleState, frameRate) } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
In the example above, the rendering is still tied to the rectangle's timer. This is okay if there is only one timer driven animation. However, it is common to have more than one animation connected to a timer. Therefore, it makes sense to connect the rendering to a separate timer. In the example below, several images are animated on the screen at the same time. It does not make sense to call the render code every time that any of these images animates. Instead, a separate timer is assigned to the render code, so that it renders independently of the animating square, triangle and circle animations. As each animation's code has been separated from the rendering code, each animation's code now only has to concern itself with the state of the animation.
Example of having a separate rendering code timer (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <script> let canvas = null let ctx = null let backgroundImage = new Image() backgroundImage.src = "images/dkit01.png" let rectangleAnimationInterval = null // set to null when not running let circleAnimationInterval = null // set to null when not running let triangleAnimationInterval = null // set to null when not running const STEP_SIZE = 1 const NUMBER_OF_FRAMES_PER_SECOND = 25 let frameRate = 1000 / NUMBER_OF_FRAMES_PER_SECOND let rectangleX = 0 let circleX = 0 let triangleX = 0 window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // hide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight renderCanvas() startRectangleAnimationTimer() startCircleAnimationTimer() startTriangleAnimationTimer() setInterval(renderCanvas, frameRate) } function renderCanvas() { // only include drawing code here ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height) renderSquare() renderCircle() renderTriangle() } function renderSquare() { // Only contains rendering code ctx.beginPath() ctx.fillStyle = "black" ctx.fillRect(rectangleX, 30, 20, 20) ctx.closePath() } function renderCircle() { // Only contains rendering code ctx.beginPath() ctx.fillStyle = "red" ctx.arc(circleX, 70, 10, 0, Math.PI * 2) ctx.fill() ctx.closePath() } function renderTriangle() { // Only contains rendering code ctx.beginPath() ctx.fillStyle = "blue" /* stroke colour */ ctx.moveTo(triangleX + 10, 100) ctx.lineTo(triangleX + 20, 120) ctx.lineTo(triangleX, 120) ctx.lineTo(triangleX + 10, 100) ctx.lineTo(triangleX + 20, 120) ctx.fill() ctx.closePath() } function updateRectangleState() { // test to see if all of the frames have been played if (rectangleX > (canvas.width + 1)) { clearInterval(rectangleAnimationInterval) rectangleAnimationInterval = null // set to null when not running } else // render the current frame and increment currentFrame { // render currentFrame //renderCanvas() // Not needed // increment the currentFrame rectangleX += STEP_SIZE } } function updateCircleState() { // test to see if all of the frames have been played if (circleX > (canvas.width + 1)) { clearInterval(circleAnimationInterval) circleAnimationInterval = null // set to null when not running } else // render the current frame and increment currentFrame { circleX += STEP_SIZE } } function updateTriangleState() { // test to see if all of the frames have been played if (triangleX > (canvas.width + 1)) { clearInterval(triangleAnimationInterval) triangleAnimationInterval = null // set to null when not running } else // render the current frame and increment currentFrame { triangleX += STEP_SIZE } } function startRectangleAnimationTimer() { if (rectangleAnimationInterval === null) { rectangleAnimationInterval = setInterval(updateRectangleState, 100) // fast } } function startCircleAnimationTimer() { if (circleAnimationInterval === null) { circleAnimationInterval = setInterval(updateCircleState, 500) // medium } } function startTriangleAnimationTimer() { if (triangleAnimationInterval === null) { trianageAnimationInterval = setInterval(updateTriangleState, 1200) // slow } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
Instead of using a timer, we can ask the system to take full control of when frames will be displayed. This will synch the code's diplay updating with the system screen refresh. This will result in smoother animations.
Separating the animation display from the event timers allows us to write cleaner code, as it allows us to place all of the drawing code separate to the state code. In the example below, the renderCanvas() function only deals with drawing items onto the canvas. The timer function updateRectanglePosition() is used to update the rectangle's state data.
Example of requestAnimationFrame() (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <script> let canvas = null let ctx = null let animationInterval = null // set to null when not running const STEP_SIZE = 1 const NUMBER_OF_FRAMES_PER_SECOND = 25 let frameRate = 1000 / NUMBER_OF_FRAMES_PER_SECOND let x = null window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // hide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight renderCanvas() startAnimationTimer() } function renderCanvas() { // clear any previous animation ctx.clearRect(0, 0, canvas.width, canvas.height) // draw the rectangle in its current position ctx.fillRect(x, 100, 50, 50) /* Continuously call requestAnimationFrame() to keep rendering the canvas */ requestAnimationFrame(renderCanvas) // recursively call next frame } function updateRectanglePosition() { // test to see if all of the frames have been played if (x > (canvas.width + 1)) { clearInterval(animationInterval) animationInterval = null // set to null when not running } else // render the current frame and increment currentFrame { // Note that we do not render the currentFrame here. // Instead, we use requestAnimationFrame() // ctx.clearRect(0, 0, canvas.width, canvas.height) // increment the currentFrame x += STEP_SIZE } } function startAnimationTimer() { if (animationInterval === null) { x = 0 animationInterval = setInterval(updateRectanglePosition, frameRate) } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
The code below shows a simple image and text animation.
The image and the text each has its own delay prior to running. For example, the function setTimeout(startImageAnimation, 2000) will cause the image animation to start running after two seconds (2000 millisconds).
Each animation should be given its own animation code. For example, updateImageAnimation() controls the behaviour of the image animation. This control includes the speed of the animation and the stop-condition of the animation.
Example code to animate an image and text (Run Example).
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <script> let canvas = null let ctx = null window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Start the animations */ setTimeout(startImageAnimation, 0) setTimeout(startTextAnimation, 2700) renderCanvas() } /* Step 2 of 3 */ /* Each animation needs its own code */ /******************************************************************************/ /* These three are ALWAYS needed */ let imageAnimationInterval = null const IMAGE_FRAMERATE = 5 // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ let imageAnimationIsDisplayed = false /* These variables depend on the animation */ let image = new Image() image.src = "images/dkit04.png" let imageX = 0 let imageY = 0 let size = 0 function startImageAnimation() { imageAnimationIsDisplayed = true imageAnimationInterval = setInterval(updateImageAnimation, IMAGE_FRAMERATE) } function stopImageAnimation() { imageAnimationIsDisplayed = true clearInterval(imageAnimationInterval) imageAnimationInterval = null // set to null when not running } function stopAndHideImageAnimation() { stopImageAnimation() imageAnimationIsDisplayed = false } function updateImageAnimation() { size++ if (size === canvas.width) { stopImageAnimation() } } function renderImageAnimation() { if (imageAnimationIsDisplayed) { ctx.drawImage(image, imageX, imageY, size, size) } } /******************************************************************************/ /******************************************************************************/ /* These three are ALWAYS needed */ let textAnimationInterval = null const TEXT_FRAMERATE = 5 // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ let textAnimationIsDisplayed = false let textX = 500 // canvas width let minTextX = 35 let textY = 300 let message = "DkIT" let fontSize = 200 let textColour = "red" /* Start the animation */ function startTextAnimation() { textAnimationIsDisplayed = true textAnimationInterval = setInterval(updateTextAnimation, TEXT_FRAMERATE) } function stopTextAnimation() { textAnimationIsDisplayed = true clearInterval(textAnimationInterval) textAnimationInterval = null // set to null when not running } function stopAndHideTextAnimation() { stopAndHideTextAnimation() textAnimationIsDisplayed = false } function updateTextAnimation() { textX-- if (textX === minTextX) { stopTextAnimation() } } function renderTextAnimation() { if (textAnimationIsDisplayed) { ctx.fillStyle = textColour ctx.font = fontSize + "px Times Roman" ctx.fillText(message, textX, textY) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Drawn the animations */ renderImageAnimation() renderTextAnimation() } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
In the above example, we can only use each function name once. Because each function name can only be used once, we need to come up with unique names, such as startImageAnimation() and startTextAnimation(). This becomes very messy when we have several animations. By using object oriented code, it is possible to contain all of the animation code for each animation inside a single object. Object oriented code will allow us to use the same function name for different objects, so that we can have a start() function inside both an ImageAnimation object and a TextAnimation. As shown in the example below, this results in much cleaner code.
The example below shows how to convert the above example into object oriented code.
Example of an object oriented animation (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <script> let canvas = null let ctx = null let image = new Image() image.src = "images/dkit04.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new ImageAnimation(0) animation[1] = new TextAnimation(2700) renderCanvas() } /* Step 2 of 3 */ /* Each animation object needs its own code */ /******************************************************************************/ /* ImageAnimation object */ class ImageAnimation { constructor(animationStartDelay) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = 5 // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* These variables depend on the animation */ this.x = 0 this.y = 0 this.size = 0 /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } update() { this.size++ if (this.size >= canvas.width) { this.stop() } } render() { ctx.drawImage(image, this.x, this.y, this.size, this.size) } } /******************************************************************************/ /******************************************************************************/ /* TextAnimation object */ class TextAnimation { constructor(animationStartDelay) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = 5 // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* These variables depend on the animation */ this.x = canvas.width this.minX = 35 this.y = 300 this.message = "DkIT" this.fontSize = 200 /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } update() { this.x-- if (this.x === this.minX) { this.stop() } } render() { ctx.fillStyle = "red" ctx.font = this.fontSize + "px Times Roman" ctx.fillText(this.message, this.x, this.y) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0; i < animation.length; i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
The TextAnimation and ImageAnimation objects in the above code both use much of the same code. All of the functions, except for update() and render(), are exactly the same for the two objects. The code that is the same for both objects can be extracted to a higher-level object. In the example below, the higher-level class is called Animation. Importantly, no matter how many animations we have, they will all extend from the same Animation class. The code that remains in the individual animations will only have functions for update() and render(). As a result, it will be much easier to develop and maintain. In the example below, only the code highlighted in red depends on the specific animations that we are running. The code rest of the code is the same for every animation that we make. This provides us with a template that we can use for all of our animations.
Example using class inheritance (Run example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black width:500px height:500px } #loadingMessage { position:absolute top:100px left:100px z-index:100 font-size:50px } </style> <script> let canvas = null let ctx = null let image = new Image() image.src = "images/dkit04.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new ImageAnimation(0) animation[1] = new TextAnimation(2700) renderCanvas() } /* Step 2 of 3 */ /* Each animation object needs its own code */ /******************************************************************************/ /* Animation */ class Animation { constructor(animationStartDelay, frameRate) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = frameRate // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } } /* ImageAnimation object */ class ImageAnimation extends Animation { constructor(animationStartDelay) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = 0 this.y = 0 this.size = 0 } update() { this.size++ if (this.size >= canvas.width) { this.stop() } } render() { ctx.drawImage(image, this.x, this.y, this.size, this.size) } } /******************************************************************************/ /******************************************************************************/ /* TextAnimation object */ class TextAnimation extends Animation { constructor(animationStartDelay) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = canvas.width this.minX = 35 this.y = 300 this.message = "DkIT" this.fontSize = 200 this.colour = "red" } update() { this.x-- if (this.x === this.minX) { this.stop() } } render() { ctx.fillStyle = this.colour ctx.font = this.fontSize + "px Times Roman" ctx.fillText(this.message, this.x, this.y) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0 i < animation.length i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
We can move the code for the various animations into seperate javascript files. This will make it much easier to maintain the code. In the example below, only the code highlighted in red needs to change for different animations.
Example of object animation code moved into seperate javascript files (Run Example)
index.html file
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black; width:500px; height:500px; } #loadingMessage { position:absolute; top:100px; left:100px; z-index:100; font-size:50px; } </style> <!-- Move each object to its own javaScript file --> <script src="js/Animation.js"></script> <script src="js/ImageAnimation.js"></script> <script src="js/TextAnimation.js"></script> <script> let canvas = null let ctx = null /* Declare any images, video or audio that will be used by any of the animations */ let image = new Image() image.src = "images/dkit04.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new ImageAnimation(0, canvas.width) animation[1] = new TextAnimation(2700, canvas.width) renderCanvas() } /* Step 2 of 3 */ /* Each animation object needs its own code */ /* The code for the various animations is now moved to seperate javascript files */ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0; i < animation.length; i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>Animation.js file
/* Animation */ class Animation { constructor(animationStartDelay) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = 5 // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } }ImageAnimation.js file
/* ImageAnimation object */ class ImageAnimation extends Animation { constructor(animationStartDelay) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = 0 this.y = 0 this.size = 0 } update() { this.size++ if (this.size >= canvas.width) { this.stop() } } render() { ctx.drawImage(image, this.x, this.y, this.size, this.size) } }TextAnimation.js file
/* TextAnimation object */ class TextAnimation extends Animation { constructor(animationStartDelay) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = canvas.width this.minX = 35 this.y = 300 this.message = "DkIT" this.fontSize = 200 this.colour = "red" } update() { this.x-- if (this.x === this.minX) { this.stop() } } render() { ctx.fillStyle = this.colour ctx.font = this.fontSize + "px Times Roman" ctx.fillText(this.message, this.x, this.y) } }
Note that the animation code in the examples that follow in these notes has been kept with the main code. This is to make it easy for students to cut and paste the code. In the real-world, they should be placed in seperate files.
We can give an object a set of initial (constructor) values. In the example above, we can pass in details of the text colour, font size, message, etc., as shown below.
Example of an object oriented animation with constructor parameters (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black width:500px height:500px } #loadingMessage { position:absolute top:100px left:100px z-index:100 font-size:50px } </style> <script> let canvas = null let ctx = null let image = new Image() image.src = "images/dkit04.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new ImageAnimation(0) animation[1] = new TextAnimation(2700, canvas.width, 35, 300, "DkIT", 200, "red") renderCanvas() } /* Step 2 of 3 */ /* Each animation object needs its own code */ /******************************************************************************/ /* Animation */ class Animation { constructor(animationStartDelay, frameRate) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = frameRate // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } } /******************************************************************************/ /******************************************************************************/ /* ImageAnimation object */ class ImageAnimation extends Animation { constructor(animationStartDelay) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = 0 this.y = 0 this.size = 0 } update() { this.size++ if (this.size >= canvas.width) { this.stop() } } renderObject() { this.render() } render() { ctx.drawImage(image, this.x, this.y, this.size, this.size) } } /******************************************************************************/ /******************************************************************************/ /* TextAnimation object */ class TextAnimation extends Animation { constructor(animationStartDelay, startX, endX, y, message, fontSize, colour) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = startX this.minX = endX this.y = y this.message = message this.fontSize = fontSize this.colour = colour } update() { this.x-- if (this.x === this.minX) { this.stop() } } render() { ctx.fillStyle = this.colour ctx.font = this.fontSize + "px Times Roman" ctx.fillText(this.message, this.x, this.y) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0 i < animation.length i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
Using parameters to set an object's initial behaviour allows us to make multiple instances of an object. In the example below, we change the way that the text looks for each of three words. Note that we only need to develop the object code once. The difference between each object is determined by the set of parameters that we send to each object.
Example that shows multiple instances of an object Run Example
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black width:500px height:500px } #loadingMessage { position:absolute top:100px left:100px z-index:100 font-size:50px } </style> <script> let canvas = null let ctx = null let image = new Image() image.src = "images/dkit04.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new TextAnimation(0, canvas.width, 135, 100, "Welcome", 60, "red") animation[1] = new TextAnimation(2300, canvas.width, 135, 300, "to", 300, "green") animation[2] = new TextAnimation(4300, canvas.width, 1, 480, "DkIT", 230, "blue") renderCanvas() } /* Step 2 of 3 */ /* Each animation object needs its own code */ /******************************************************************************/ /* Animation */ class Animation { constructor(animationStartDelay, frameRate) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = frameRate // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } } /******************************************************************************/ /******************************************************************************/ /* TextAnimation object */ class TextAnimation extends Animation { constructor(animationStartDelay, startX, endX, y, message, fontSize, colour) { super(animationStartDelay, 5) /* These variables depend on the animation */ this.x = startX this.minX = endX this.y = y this.message = message this.fontSize = fontSize this.colour = colour } update() { this.x-- if (this.x === this.minX) { this.stop() } } render() { ctx.fillStyle = this.colour ctx.font = this.fontSize + "px Times Roman" ctx.fillText(this.message, this.x, this.y) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0 i < animation.length i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
Object state data should be set as constructor parameters where possible. If should only be initialised as local object data if it cannot be different for different versions of the object. To achieve this, you should always look at the data in the constructor() method and decide if it can be variable or if it is fixed. For example, if you were asked to write code to get an image to pulse by a distance of 20 pixels from full canvas size, you might produce the code below.
Code to pulsate an image, as shown here (Run Example).
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black width:500px height:500px } #loadingMessage { position:absolute top:100px left:100px z-index:100 font-size:50px } </style> <script> let canvas = null let ctx = null let backgroundImage = new Image() backgroundImage.src = "images/dkit03.png" let image = new Image() image.src = "images/dkit01.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new PulsatingImage(0) renderCanvas() } /* Step 2 of 3 */ /* Each animation needs its own code */ /******************************************************************************/ /* Animation */ class Animation { constructor(animationStartDelay, frameRate) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = frameRate // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } } /******************************************************************************/ /******************************************************************************/ /* PulsatingImage Object */ class PulsatingImage extends Animation { constructor(animationStartDelay) { super(animationStartDelay, 15) /* These variables depend on the animation */ this.pulseNumberOfSteps = 20 this.startX = 0 this.x = 0 this.y = 0 this.size = canvas.width this.increment = 1 } update() { this.x += this.increment this.y += this.increment this.size = canvas.width - (this.x * 2) if (this.x >= (this.startX + this.pulseNumberOfSteps)) { this.increment = -1 } else if (this.x <= this.startX) { this.increment = 1 } } render() { ctx.drawImage(image, this.x, this.y, this.size, this.size) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0 i < animation.length i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
From the above example, we can see that the ImageAnimation constructor contains the data below:
constructor(animationStartDelay)
{
super(animationStartDelay, 15)
/* These variables depend on the animation */
this.pulseNumberOfSteps = 20
this.startX = 0
this.x = 0
this.y = 0
this.size = canvas.width
this.increment = 1
}
We can pass pulseNumberOfSteps, startX, startY and size as parameters to the object. We do not need to change increment, as it will always start as 1. This will result in the version of the code below:
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black width:500px height:500px } #loadingMessage { position:absolute top:100px left:100px z-index:100 font-size:50px } </style> <script> let canvas = null let ctx = null let backgroundImage = new Image() backgroundImage.src = "images/dkit03.png" let image = new Image() image.src = "images/dkit01.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new PulsatingImage(0, 20, 0, 0, canvas.width) renderCanvas() } /* Step 2 of 3 */ /* Each animation needs its own code */ /******************************************************************************/ /* Animation */ class Animation { constructor(animationStartDelay, frameRate) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = frameRate // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } } /* PulsatingImage Object */ class PulsatingImage extends Animation { constructor(animationStartDelay, pulseNumberOfSteps, startX, startY, size) { super(animationStartDelay, 15) /* These variables depend on the animation */ this.pulseNumberOfSteps = pulseNumberOfSteps this.startX = startX this.x = startX this.y = startY this.size = size this.increment = 1 } update() { this.x += this.increment this.y += this.increment this.size = canvas.width - (this.x * 2) if (this.x >= (this.startX + this.pulseNumberOfSteps)) { this.increment = -1 } else if (this.x <= this.startX) { this.increment = 1 } } render() { ctx.drawImage(image, this.x, this.y, this.size, this.size) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0 i < animation.length i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
Both versions of the code above do the exact same thing. However, the second version is much more useful.
Explain why the second version of the code (with the object state data passed in as parameters to the object) is more useful than the first version of the code.
Write code to rotate an image, as shown here.
The variable ctx.globalAlpha can be used to control the opacity of a canvas drawing. Amend the code from the previous question so that the rotating image fades in, as shown here.
Write code to produce a scrolling background image, as shown here.
Write code to produce a graphical countdown timer, as shown here.
If an animation consists of various related sub-animations, then it can makes sense to control all of the sub-animations with the same timer.
Write code to animate four images as shown here. Note that, as all four images are always the same size as each other, it is possible to use only one timer.
Amend the above code to animate four parts of the same image, as shown here. Hint: use ctx.drawImage(image, clipX, clipY, clipWidth, clipHeight, x, y, width, height). This allows you draw a clipped sub-section of an image.
Write code to make a slider with four images, as shown here.
Write code to make a square slider with four images, as shown here.
Write code to make a diagonal slider with four images, as shown here.
A sprite animation is made up of a set of sub-images that are combined into one master-image, as shown below.
In order to animate a sprite, we need to be able step through each sub-image in turn. This can be done using the code below:
const NUMBER_OF_SPRITES = 74 // the number of sprites in the sprite image const NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 9 // the number of columns in the sprite image const NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 9 // the number of columns in the sprite sprite const START_ROW = 0 const START_COLUMN = 0 let currentSprite = 0 // the current sprite to be displayed from the sprite image let row = START_ROW // current row in sprite image let column = START_COLUMN // current column in sprite image this.update = update function update() { if(currentSprite === NUMBER_OF_SPRITES) { stopAndHide() } currentSprite++ column++ if (column >= NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE) { column = 0 row++ } }
The code below showns an animation of the explosion sprite (Run Example)
<!DOCTYPE html> <html> <head> <title>Course notes example code</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #canvas { border:1px solid black width:500px height:500px } #loadingMessage { position:absolute top:100px left:100px z-index:100 font-size:50px } </style> <script> let canvas = null let ctx = null let explosionImage = new Image() explosionImage.src = "images/explosion.png" /* Animation array */ let animation = [] window.onload = onAllAssetsLoaded document.write("<div id='loadingMessage'>Loading...</div>") function onAllAssetsLoaded() { // stopAndHide the webpage loading message document.getElementById('loadingMessage').style.visibility = "hidden" canvas = document.getElementById("canvas") ctx = canvas.getContext("2d") canvas.width = canvas.clientWidth canvas.height = canvas.clientHeight /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new ExplosionAnimation(0, 150, 250, 200) // there are two explosions in this example animation[1] = new ExplosionAnimation(1000, 200, 300, 100) renderCanvas() } /* Step 2 of 3 */ /* Each animation needs its own code */ /******************************************************************************/ /* Animation */ class Animation { constructor(animationStartDelay, frameRate) { /* These variables are ALWAYS needed */ this.animationInterval = null this.frameRate = frameRate // change to suit the animation frameRate in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay) } /* Public functions */ start() { this.animationIsDisplayed = true this.animationInterval = setInterval(this.update.bind(this), this.frameRate) } stop() { this.animationIsDisplayed = true clearInterval(this.animationInterval) this.animationInterval = null // set to null when not running } stopAndHide() { this.stop() this.animationIsDisplayed = false } renderObject() { if (this.animationIsDisplayed) { this.render() } } /* update() and render() will be different for each animation */ update() { } render() { } } /* ImageAnimation Object */ class ExplosionAnimation extends Animation { constructor(animationStartDelay, centreX, centreY, size) { super(animationStartDelay, 40) /* These variables depend on the animation */ this.centreX = centreX this.centreY = centreY this.size = size this.NUMBER_OF_SPRITES = 74 // the number of sprites in the sprite image this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 9 // the number of columns in the sprite image this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 9 // the number of rows in the sprite image this.START_ROW = 0 this.START_COLUMN = 0 this.currentSprite = 0 // the current sprite to be displayed from the sprite image this.row = this.START_ROW // current row in sprite image this.column = this.START_COLUMN // current column in sprite image this.SPRITE_WIDTH = 100 this.SPRITE_HEIGHT = 100 } update() { if (this.currentSprite === this.NUMBER_OF_SPRITES) { this.stopAndHide() } this.currentSprite++ this.column++ if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE) { this.column = 0 this.row++ } this.SPRITE_WIDTH = (explosionImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE) this.SPRITE_HEIGHT = (explosionImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE) } render() { ctx.drawImage(explosionImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_WIDTH, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size) } } /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas) ctx.clearRect(0, 0, canvas.width, canvas.height) /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0 i < animation.length i++) { animation[i].renderObject() } } </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>
Write code to animate a sprite of a bird flying, as shown here.
Write code to animate a sprite of a bird flying on a scrolling background, as shown here.
A sprite's first frame can be located at any row and column in the sprite image. The FIRST_ROW and FIRST_COLUMN constants in the example above deal with this situation. A sprite image can be oriented the wrong way for the purposes of a partiular animation. Write code that rotates the green tank in the sprite image below by 90 degrees clockwise before it displays, as shown here.
In the above example, the sprite images are in reverse order. This is giving the visual effect of the tank tracks moving in reverse. Amend the above code to display the sprite images in reverse order, so that the tank tracks look as though they are moving forward, as shown here.
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.