Animations with the Canvas API - Part 3: Gravity and Dynamic Rendering

Joshua Hall

Over in Part 2 of this series we created a ball that would ricochet around the screen and change color when it collided with a border. Now we’re going to use what we learned to make this rain animation that dynamically renders drops with particle effect as each drop hits the bottom of our canvas.

Boilerplate

Since we’re going to be working so close to the bottom of the screen we should hide any horizontal scroll bars with overflow: hidden, and we’ll darken it a bit so be a bit less eye burning.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>HTML Canvas</title>
    <style>
      body {
        overflow: hidden;
        background-color: #1a202c;
      }
    </style>
  </head>
  <body>
      
    <canvas></canvas>

  </body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
  <script src="./canvas.js"></script>
</html>

canvas.js

// Utilities
const randomNum = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
const randomColor = colors => colors[Math.floor(Math.random() * colors.length)];

// Get canvas element
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');

// Make canvas fullscreen
canvas.width = innerWidth;
canvas.height = innerHeight;
addEventListener('resize', () => {
  canvas.width = innerWidth;
  canvas.height = innerHeight;
});

// Control Panel
const gui = new dat.GUI();

const controls = {
  count: 0,
  velocity: 0,
};

gui.add(controls, 'dx', 0, 10);
gui.add(controls, 'dy', 0, 10);

// New Object
class Ball {
  constructor(x, y, radius, color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }
};

Ball.prototype.draw = function () {
  c.beginPath();
  c.fillStyle = this.color;
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.fill();
  c.closePath();
};

Ball.prototype.update = function () {
  this.x += controls.dx;
  this.y += -controls.dy;
  this.draw();
};

const ball = new Ball(innerWidth / 2, innerHeight / 2, 50, 'red');

// Handle changes
const animate = () => {
  requestAnimationFrame(animate);

  c.clearRect(0, 0, canvas.width, canvas.height);

  ball.update();
};

animate();

Drop

Let’s start by getting our main drops working. We just need to store all of the variables for each drop as an object, draw a line on the screen, and add some value to the y position whenever update is ran to make it move downward.

canvas.js

class Drop {
  constructor(x, y, dy, thickness, length, color) {
    this.x = x;
    this.y = y;
    this.dy = dy;
    this.thickness = thickness;
    this.length = length;
    this.color = color;
  }
};

Drop.prototype.draw = function () {
  c.beginPath();
  c.strokeStyle = this.color;
  c.lineWidth = this.thickness;
  c.moveTo(this.x, this.y);
  c.lineTo(this.x, this.y - this.length);
  c.stroke();
  c.closePath();
}

Drop.prototype.update = function () {
  this.y += this.dy;

  this.draw();
}

Let’s render one onto the center of the canvas screen to see if it’s working.

const drop = new Drop(innerWidth / 2, innerHeight / 2, 2, 5, 30, 'red');

const animate = () => {
  requestAnimationFrame(animate);
  c.clearRect(0, 0, canvas.width, canvas.height);

  drop.update();
};

animate();

single drop

Ticker

That’s nice, but we’re going to need a lot more rendering along the top. To have multiple drops we could just make an array, use a for loop to assign random values to each item before pushing them to the array, and use forEach to update each of them per frame. But just using a for loop would only render the drops once, which would all move past our canvas out of site. So we have to be a bit creative to continually add new drops while removing all drops that move below our screen.

To do this, we’re going to make a ticker that will count up by one every frame, every time it’s perfectly divisible by some number we’ll add a new drop to the array. Whatever number we divide it by will control how often new drops are rendered. To remove them, and save on processing power, we’ll just splice them out of the array when they’re past the bottom.

Using the modulo operator (%) we can divide a number and check if the remainder equals 0. So the higher it is the less often new drops will be rendered.

While we’re here let’s give them some color. I find that using different values of the same color, along with random thickness and length, helps to give the illusion of some depth. I recommend checking out Kuler for your color palettes.

canvas.js

const colors = [ '#9C4AFF', '#8C43E6', '#7638C2', '#5E2C99', '#492378'];

let drops = [];
let ticker = 0;
const animate = () => {
  requestAnimationFrame(animate);
  // Try using the 'residue' effect from Part 2
  // c.fillStyle = 'rgba(33, 33, 33, .3)'; //Lower opacity creates a longer tail
  // c.fillRect(0, 0, canvas.width, canvas.height);
  c.clearRect(0, 0, canvas.width, canvas.height);

  drops.forEach((drop, index) => {
    drop.update();
    if (drop.y >= canvas.height) drops.splice(index, 1);
  });

  // Timing between drops
  ticker++;
  let count = controls.count === 0 ? 0 : randomNum(controls.count + 5, controls.count);
  if (ticker % count == 0) {
    const x = randomNum(0, innerWidth);
    const y = 0;
    const dy = controls.velocity === 0 ? 0 : randomNum(controls.velocity, controls.velocity + 10);
    const thickness = randomNum(3, 5);
    const length = randomNum(20, 50);

    drops.push(new Drop(x, y, dy, thickness, length, randomColor(colors)));
  };
};

Droplets and Gravity

To create our splash effect as they hit the ground we’re going to need some smaller particles that will have a gravity-like effect, arcing out from the main drop. Our Droplet class is going to be pretty similar to our main Drop, with a few differences since the droplets will be circles instead of lines.

canvas.js

class Droplet {
  constructor(x, y, dx, dy, radius, color) {
    this.x = x;
    this.y = y;
    this.dx = dx;
    this.dy = dy;
    this.radius = radius;
    this.color = color;
    this.gravity = .1;
  }
};

Droplet.prototype.draw = function () {
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
};

The main thing we want to worry about is our gravity. Gravity causes our downward movement to increase, so we’ll want to add this to our dy on the update method. So when we generate many droplets moving up, with a negative dy value, and add our gravity value onto it every frame it will slow down, reverse direction, and speed up until it is removed past our canvas. I made a simpler example with just the droplets, you can experiment with here.

Droplet.prototype.update = function () {
  this.dy += this.gravity;
  this.y += this.dy;
  this.x += this.dx;

  this.draw();
};

And we’ll update and remove them just like our main drops.

let droplets = [];
const animate = () => {
  droplets.forEach((droplet, index) => {
    droplet.update();
    if (droplet.y >= canvas.height) droplets.splice(index, 1);
  });
};

Splash

Adding our particles is actually very simple, we can just use a for loop to generate them with the main drops position and pass-in some random values for the rest. Let’s also add some gravity to our drops to make them fall a bit more realistically.

canvas.js

class Drop {
  constructor(x, y, dy, thickness, length, color) {
    this.x = x;
    this.y = y;
    this.dy = dy;
    this.thickness = thickness;
    this.color = color;
    this.length = length;
    this.gravity = .4;
  }
};

Drop.prototype.update = function () {
  // Stops drops if velocity controller is set to 0
  if (this.dy > 0) this.dy += this.gravity;
  this.y += this.dy;

  // It runs splash over the whole length of the drop, to we'll narrow it down to the end.
  if (this.y > canvas.height - 100) this.splash(this.x, this.y + (this.length * 2));

  this.draw();
}

Drop.prototype.splash = function (x, y) {
  for (let i = 0; i < 5; i++) {
    const dx = randomNum(-3, 3);
    const dy = randomNum(-1, -5);
    const radius = randomNum(1, 3);

    droplets.push(new Droplet(x, y, dx, dy, radius, randomColor(colors)));
  };
};

Conclusion

While there’s still an enormous amount he learn about HTML canvas, hopefully this short series was a gentle enough introduction to its possibilities. Most sites are similar in a lot of ways but the ability to create custom animations offers a uniqueness that the most popular automated site builder tools never will.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#