Let's Make it Snow!
Tags: webdev, canvas, typescript, tutorial
Published: 2020-12-10 17:01:36

---
title: Let's Make it Snow!
published: true
description: A simple JavaScript snowing animation using HTML5 Canvas
tags: webdev, canvas, typescript, tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/5lhzhm1odp74xnswsqec.gif
---

One of the first things I ever programmed was a snow animation. That was over 20 years ago and written in QBasic, but I always come back to the simplicity of a serene snowfall scene. So, when the opportunity arose this year to spice up a simple web page sharing some holiday photos with family and friends, I decided to go ahead and "reboot" one of my very first software projects, and bring it to the web in style.

---

<center>_**Need Instant Gratification?**<br>This post is written as a tutorial, building up our snow scene through iteration. If you'd like to skip all that, you can head down to [the "final" CodePen](#codepen) immediately._</center>

---

## The Tech & Approach
The goal here is to create a performant snow animation that has a natural look and feel and doesn't physically get in the way of the content on the page.

The basic approach to animation in general is to have a subject or subjects, and update their look many, many times a second. Luckily, snow flakes are a pretty simple subject, and the web has some built in mechanisms to handle animation, framerate, and drawing.

So, our basic approach will be to have an **Array** of **snow flake data**, which we will iterate over each snow flake, update their position, and draw them within an **HTML5 Canvas** for each frame, which will be controlled using **requestAnimationFrame**.

Let's start by defining our snowflake. In order for our snowflake to be drawn, we need to know where it exists on our canvas. For this, we'll start with a simple interface for its x and y position:

```typescript
interface SnowFlake {
  /** The current x position. */
  x: number;
  /** The current y position. */
  y: number;
}
```

> _**Note:** We're using TypeScript here. Though, this whole project is quite simple and can easily be converted to raw JavaScript without too much effort._


Next, we need a `<canvas>` element, and attach it to the dom. We'll also add some styles so it fills the viewport, stays-put with any scrolling, is on top of any content, and doesn't get in the way of any clicks.

```typescript
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.width = window.innerHeight;
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '999';
document.body.appendChild(canvas);
```

Also, since we're using a whole lot of random values, we'll make a couple quick helper functions:

```typescript
/** Helper function returning a decimal between min/max. */
function random(min: number, max: number) {
  return Math.random() * (max - min) + min;
}

/** Helper function returning an int between & inclusive of min/max. */
function randomInt(min: number, max: number) {
  return (Math.floor(Math.random() * (max - min + 1)) + min);
}
```

Now, let's generate all our snow flakes with some random `x` and `y` values.

```typescript
const flakes: SnowFlake[] = [];
const numOfFlakes = randomInt(300, 600);
for (var i = 0; i < numOfFlakes; i++) {
  flakes.push({
    x: randomInt(0, canvas.width),
    y: randomInt(0, canvas.height),
  });
}
```

Finally, we'll create our `draw` function which we'll call over and over again using `window.requestAnimationFrame`.

```typescript
const ctx = canvas.getContext('2d')!;
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#fff';
  ctx.beginPath();
  flakes.forEach((flake) => {
    // Draw our flake at its current x/y
    ctx.moveTo(flake.x, flake.y);
    ctx.arc(flake.x, flake.y, 2, 0, Math.PI * 2);

    // Update our flake's next x/y.
    flake.y += 1;
    flake.x += 1;
    // If our snowflake goes off the left, right or bottom,
    // move it to the opposite side.
    if (flake.x > canvas.width) {
      flake.x = 0;
    } else if (flake.x < 0) {
      flake.x = canvas.width;
    } else if (flake.y > canvas.height) {
      flake.x = randomInt(0, canvas.width);
      flake.y = -2;
    }
  });
  // Fill in all the arc paths' we've just created...
  ctx.fill();
  // ...and schedule us to do it all over again.
  window.requestAnimationFrame(draw);
}
```

And finally, kick it off:

```typescript
window.requestAnimationFrame(draw);
```

---

**That's it!** We now have snow! 
{% codepen https://codepen.io/rgthree/pen/MWjjZzw default-tab=full %}


## Wait... Is that really it?
Of course not! There's so much more we can do.

Alright, let's keep going. I'm going to walk us through a bunch of improvements and you should follow along and see how our snow scene starts to take shape.

### 1) Let's add some dynamic sizes.
We're going to add a `radius` field to our `SnowFlake` interface, set a random size when we generate our flakes, and use this field in our `draw` function (replacing the hardcoded "2" there).

```typescript
interface SnowFlake {
  // ...
  /** The radius in pixels. */
  radius: number;
}

// ... and our new flake generation will look like the following:
flakes.push({
  x: randomInt(0, canvas.width),
  y: randomInt(0, canvas.height),
  radius: random(.25, 2),
});

// ...and we'll use that in our draw method for the arc:
ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
```

**Awesome!** When you plug these changes in you'll see much more dynamically sized snow. It's already feeling better!

But there's still something missing... they all still move the same direction and at the same speed...


### 2) Let's add some dynamic movements!
Alright, we're going to do something similar to make the snow move more randomly. Just like we added `radius` above, we'll now add `drop` and `sway` fields as well. These will be used to offset the general direction we're moving so we get some flakes that move a little faster, both vertically (drop) and horizontally (sway).

```typescript
interface SnowFlake {
  // ...
  /** The radius in pixels. */
  radius: number;
  /** A value to add to y movement to speed/slow itself. */
  drop: number;
  /** A value to add to x movement to speed/slow itself. */
  sway: number;
}

// ... and our new flake generation will look like the following.
// (Similarly, we'll inflate our range and divide by 100 to get
// a number between -.300 and .300 with a precision of three).
flakes.push({
  x: randomInt(0, canvas.width),
  y: randomInt(0, canvas.height),
  radius: random(.25, 2),
  sway: random(-.3, .3),
  drop: random(-.3, .3),
});

// ...and now, in our `draw` method, when we update our position,
// we'll add the sway and drop:
flake.y += 1 + flake.drop;
flake.x += 1 + flake.sway;
```

And now we have dynamically falling and swaying snowflakes. It looks much much better already! There's one last thing we can do to smooth out one rough edge...

### 4) Let's fix resizing.
You may have noticed if you resize the window, the snow stretches instead of applying the extra/lost room. This is because the canvas' CSS is stretching the element, but its set width and height don't update, so our drawing becomes stretches or squished.

This is actually a simple fix, which we do so by setting the `width` and `height` attributes of the canvas to it's new size when we resize:

```typescript

// Update the canvas width/height data when the window resizes.
window.addEventListener('resize', () => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
});

```

---

**Now that's it!** We have dynamic, more natural feeling snow! 
{% codepen https://codepen.io/rgthree/pen/vYXXvoa default-tab=full %}


## Hold on... Is that really it?
You got me! There's always more we can do. But this is a terrific spot to stop and deliver. We'll call what we've just created the **MVP**. Wrap it up in a bow and push it to production :)

---

As I mentioned at the very beginning, I created my version of falling snow for a holiday webpage I was sharing with family and friends. What I actually built was a little slightly different than the tutorial walkthrough above, which I've simplified a bit. 

<a name="codepen"></a> Below, we have the my "final" CodePen, which has some additional enhancements like a gentle wind causing the snowflakes to slowly sway from left to right, etc. I also have it all in a `class` instance with `start`, `pause`, and `stop` methods, because that's how I enjoy encapsulating code. You're welcome to check it out below, and think up even more ways to make it better for next holiday season!

{% codepen https://codepen.io/rgthree/pen/poEExXW default-tab=full %}