loading...
Cover image for Creating a canvas animation, understanding the basics of animation.

Creating a canvas animation, understanding the basics of animation.

rafinskipg profile image Rafael Pedrola Gimeno ・6 min read

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 a context.rotate(radians)

  • Draw a square with context.rect(x, y, width, height)

  • Fill the square with color with context.fillStyle = 'green' and context.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)
  })
}

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

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.

Posted on by:

rafinskipg profile

Rafael Pedrola Gimeno

@rafinskipg

CoFounder of Etéreo (https://etereo.io) and Creator of Edge.

Discussion

pic
Editor guide