Tutorial

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

Published on October 6, 2019
Default avatar

By Joshua Hall

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

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Joshua Hall

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel