DEV Community

loading...
Cover image for Let it snow

Let it snow

vixalien
・4 min read

Originaly posted on my blog

Go to https://vixalien.ga/post/let-it-snow#snow to view the results

🌨⛄ Do you like snow? Does it snow in your region? Are we in December yet?

We are going to create virtual snow using the chilly Web Animations API.

A snowflake!

First and foremost, let's create a snowflake! Our snowflake will be loaded as an .svg file provided by the beautiful Ionicons.

Loading the snowflake

You can store it as a local file then load it as SVG, or use it from Ionicon's library, but we will be storing it as a string.

let svg_str = `<!-- snowflake svg text here -->`;
Enter fullscreen mode Exit fullscreen mode

Parsing the string into a DOM element

Then we'll use DOMParser to parse the string into an actual DOM element.

let snow = new DOMParser().parseFromString(svg_str, "text/xml").children[0];
Enter fullscreen mode Exit fullscreen mode

Note: Because parseFromString returns a #document, we used .children[0] to get the <svg> element instead. (<svg> is equivalent to <html>.)

Setting the snowflake to float

Our snowflake is fixed (it doesn't scroll like other elements) and initially, it is placed just above the screen.

snow.style.position = "fixed";
snow.style.top = "-24px";
Enter fullscreen mode Exit fullscreen mode

Creating a new snowflake

Because our page will have many snowflakes, we'll clone the snowflake we just created.

let newSnow = () => {
    let clonedSnow = snow.cloneNode(true);
    // we pass true to clone the node deeply (that is, with all it's children).
};
Enter fullscreen mode Exit fullscreen mode

Note: from now on, our code will be in the newSnow function.

Next, we'll generate a random left position for that snowflake

let left = Math.floor(document.body.offsetWidth * Math.random());
// we use Math.floor to ensure left is an integer
clonedSnow.style.left = left + "px";
Enter fullscreen mode Exit fullscreen mode

Then we'll just add it to the DOM

document.body.append(clonedSnow);
Enter fullscreen mode Exit fullscreen mode

Animating the snowflake

Here we'll just use Web Animations API to animate an element. To use the API, we run element.animate(keyframes, options). You can read more in the MDN Page.

To make real snow effect, we will also generate a random speed (think the animation's duration)

let time = Math.max(10 * Math.random(), 5) * 1000;
// Math.max choose the largest argument it was given. By using it here, we restrict time to be larger than 5.
Enter fullscreen mode Exit fullscreen mode

We will animate the snow to change it's top CSS property gradually. At the end, the element will be placed just below the viewport, where you can't see it.

let anim = clonedSnow.animate(
    {
        top: window.innerHeight + 24 + "px",
    },
    { duration: time, fill: "forwards" }
);
Enter fullscreen mode Exit fullscreen mode

One last thing, we'll do Garbage Collection. When the animation ends, delete that snowflake as it is no longer useful.

// garbage collection
anim.onfinish = el => el.target.effect.target.remove()
Enter fullscreen mode Exit fullscreen mode

Now go ahead, in your console, run newSnow(). You'll see a snowflake falling slowly.

Snowing!!!

So far, we can only create snowflakes on demand by running newSnow() everytime we need it. What about we create a loop that create as many snowflakes as possible?

The problem with native JS loops

If you use for loops or while or whatever, it won't work. Why? It will create many snowflakes at a time. Your browser will be filled with snowflakes and unless you are on a supercomputer, your browser will crash, badly. This creates a need for a custom loop!

Looping asynchronously

Async Iterate

Here's an implementation of an async loop.

let asyncIterate = async (start, iterations, fn) => {
    // initialize the iterator
    let i = start;
    let call = res => fn(res)
        // waits for the function to resolves before calling the next iteration
        .then(async result => {
            if (i >= iterations) return result;
            i++
            return await call(i)
        });
    return await call(i);
}
Enter fullscreen mode Exit fullscreen mode

It accepts 3 parameters. start is what the iterator is initialized as. iterations is pretty self-explanatory. it is the number of times the function will run. then fn is the function to execute.

It is important to remember that this is an async loop. That means, it will run the function, then waits that it resolves. then execute the next iteration.

wait

Next is the wait function. This is a wrapper around setTimeout. It waits some time (in milliseconds), then execute a function. (It is available on the npm registry as async-wait-then).

wait = time => new Promise(res => setTimeout(res, time))
Enter fullscreen mode Exit fullscreen mode

Here is a simple example using wait.

wait(1000)
    .then(() => console.log('This will be logged after one second!'));
Enter fullscreen mode Exit fullscreen mode

Using wait and asyncIterate to snow

By combining wait and asyncIterate, we get a powerful function set that uses the Promises API.

So, to create realistic snow (and prevent browser crashes) we'll have to wait before we create a snow element

asyncIterate(0, 10, async () => {
    await wait(1000)
    newSnow()
})
Enter fullscreen mode Exit fullscreen mode

This will make it rain 10 snowflakes, but with an interval of 1 seconds between each snowflake

To make it look more realistic (and add some suspense), we will wait for a random amount of time instead of the static 1 second.

asyncIterate(0, 10, async () => {
    await wait(Math.max(3 * Math.random(), 1) * 300)
    newSnow()
})
Enter fullscreen mode Exit fullscreen mode

But then, this will only create 10 snowflakes. Let's make it rain forever.

asyncIterate(0, Infinity, async () => {
    await wait(Math.max(3 * Math.random(), 1) * 300)
    newSnow()
})
Enter fullscreen mode Exit fullscreen mode

The full code, complete with some optimizations is posted as Github Gist

Discussion (0)