For this example i am going to use the platform glitch.com. It is a free online code editor and hosting platform, which will allow me to show you a full working example that you can edit:
Everything starts with a blank canvas:
<canvas id="canvas"></canvas>
Note: During this tutorial, I don’t want to dive into all the explanations on how canvas work, if you want to understand canvas more in-depth you should follow my leanpub page: https://leanpub.com/deceroacanvas
For now let’s just explain a basic concept about rendering.
To paint things into a canvas we need to use it’s JavaScript API. For that will get the context
and interact with it:
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
Imagine we want to pain this rotated square:
For doing that we need to:
Translate the origin of coordinates of the context with
context.translate(x, y)
followed by acontext.rotate(radians)
Draw a square with
context.rect(x, y, width, height)
Fill the square with color with
context.fillStyle = 'green'
andcontext.fill()
Stroke the square with
context.stroke()
Paint the text indicating the angle of rotation with
context.text(TEXT, x,y)
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
function drawSquare(x, y, size, angleOfRotation) {
// Translate in the context the origin of coordinates
context.translate(x, y);
// Rotate the context
const radians = Utils.degreeToRadian(angleOfRotation)
context.rotate(radians);
// Draw a square
context.beginPath();
context.rect(-Math.round(size/2), -Math.round(size/2), size, size);
context.stroke();
context.fillStyle = 'green';
context.fill();
// Paint a text indicating the degree of rotation
// (at 0, 0 because we have translate the coordinates origin)
context.fillStyle = 'black';
context.fillText(angleOfRotation, 0 , 0 );
}
function maximizeCanvas() {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
function render() {
maximizeCanvas()
drawSquare(100, 100, 100 ,10)
}
render();
You can edit this code on glitch https://glitch.com/~etereo-canvas-animation-0
We have used a function to translate degrees to radians:
Utils.degreeToRadian = function(degree) {
return degree / (180 / Math.PI);
}
If we want to have many random figures we could expand our previous example with the next code:
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const totalFigures = 50
const figures = []
function drawSquare(x, y, size, angleOfRotation) {
// Store the painting state in a stack
context.save()
// We get the radians from a degree
const radians = Utils.degreeToRadian(angleOfRotation);
// Translate in the context the origin of coordinates
context.translate(x, y);
// Rotate the context
context.rotate(radians);
// Draw a square
context.beginPath();
context.rect(-Math.round(size/2), -Math.round(size/2), size, size);
context.stroke();
context.fillStyle = Utils.randomColor();
context.fill();
// Paint a text indicating the degree of rotation (at 0, 0 because we have translate the coordinates origin)
context.fillStyle = 'black';
context.fillText(angleOfRotation, 0 , 0 );
// Restore the state of the context from the stack
context.restore()
}
function createFigures() {
for(var i = 0; i<totalFigures; i++) {
figures.push({
x: Utils.randomInteger(0, 560),
y: Utils.randomInteger(0, 560),
size: Utils.randomInteger(20, 100),
angle: Utils.randomInteger(0, 360)
})
}
}
function maximizeCanvas() {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
function render() {
maximizeCanvas()
createFigures()
figures.map(square => {
drawSquare(square.x, square.y, square.size, square.angle)
})
}
render();
In this case we introduced 2 new concepts.
context.save()
allows to preserve the state of the context before the translation and the rotation. If we don’t use context.save any consecutive rotations and translations will apply over the previous ones, producing an undesired behavior (or not, depending on the case you are trying to reproduce).context.restore()
restores the canvas to the previous state on the drawing stack.
This is what we have now:
This is kind of cool but we are not animating anything, this is just a render.
If we want to create the movement we need to change the positions or angle of rotation the figures have. We also need to invoke the render method many times.
Just like in an old movie, animation still happens because frames change over time:
To do so we need different elements:
A loop that will be executed at least 30 times per second ( frames per second), ideally at 60fps.
We will nead to “clear” or delete the previous canvas before we paint the new state.
The figures will need to update their positions based on how much time has passed since the last frame. We call this difference of time since last frame
dt
These 3 elements form the basics of animation or any animation engine.
Game engines have much more utilities but they should have this kind of concept embedded somewhere.
Let’s code!
The loop:
For the loop we are going to use requestAnimationFrame
. This method will give us a callback that will be executed after the browser finished rendering all the things.
Every time we call the loop we are going to calculate the difference of time dt
since the last execution, and we will use this time variable to calculate how much the figures should move
function loop() {
const now = Date.now()
dt = (now - before) / 1000
// update(dt)
render()
before = now
window.requestAnimationFrame(loop)
}
loop()
If we add this code we will have something like this:
The stacking of figures happens because we are not cleaning the canvas between renderings. And also we are not updating our figure positions yet.
Clearing the canvas
To clear the canvas between iterations we can use the next method:
function clear() {
context.clearRect(0, 0, canvas.width, canvas.height)
}
This will clean everything in that rectangle and we will be able to draw again:
Updating the elements
Instead of rendering new elements each time, we want to keep the same figures we initialized with createFigures
but now we are going to update their X position through time. For that we will use dt
.
In this example we are going to update the horizontal position, to know more about how to update speeds, acceleration, use vectors of movement, or things like that I suggest you take a look to the book The Nature of Code or wait for my canvas book to be complete.
function update(dt) {
const speed = 100 // We can have a different speed per square if we want
figures.forEach(figure => {
figure.x = figure.x + (dt * speed ) > canvas.width ? 0 : figure.x + (dt * speed)
})
}
```
![](https://miro.medium.com/max/1200/0*vYCdtjAK-Rzrk1Bg.gif)
Let’s take a look into the full example code.
If you want to edit it or see it working go to : https://glitch.com/~etereo-animation-canvasfinal
```javascript
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
let before = Date.now()
let dt = 0
const totalFigures = 50
const figures = []
function drawSquare(square) {
// Store the painting state in a stack
context.save()
// We get the radians from a degree
const radians = Utils.degreeToRadian(square.angle);
// Translate in the context the origin of coordinates
context.translate(square.x, square.y);
// Rotate the context
context.rotate(radians);
// Draw a square
context.beginPath();
context.rect(-Math.round(square.size/2), -Math.round(square.size/2), square.size, square.size);
context.stroke();
context.fillStyle = square.color;
context.fill();
// Paint a text indicating the degree of rotation (at 0, 0 because we have translate the coordinates origin)
context.fillStyle = 'black';
context.fillText(square.angle, 0 , 0 );
// Restore the state of the context from the stack
context.restore()
}
function createFigures() {
for(var i = 0; i<totalFigures; i++) {
figures.push({
x: Utils.randomInteger(0, 560),
y: Utils.randomInteger(0, 560),
color: Utils.randomColor(),
size: Utils.randomInteger(20, 100),
angle: Utils.randomInteger(0, 360)
})
}
}
function maximizeCanvas() {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
function update(dt) {
const speed = 100 // We can have a different speed per square if we want
// We are updating only the X position
figures.forEach(figure => {
figure.x = figure.x + (dt * speed ) > canvas.width ? 0 : figure.x + (dt * speed)
})
}
function render() {
figures.map(square => {
drawSquare(square)
})
}
function clear() {
context.clearRect(0, 0, canvas.width, canvas.height)
}
function loop() {
const now = Date.now()
dt = (now - before) / 1000
clear()
update(dt)
render()
before = now
window.requestAnimationFrame(loop)
}
// Initialize everything
createFigures()
maximizeCanvas()
loop()
```
This is all for now! You did understand how to create animations in a canvas, the rest from here is upon your imagination.
Top comments (1)
Woow, I'm enjoying this article 👏