DEV Community

Jon Kantner
Jon Kantner

Posted on • Updated on

Fitting Canvas Snow In a Tweet

Last year before Christmas, I created this snowfall animation in 257 bytes:

Output of the code

It’s not just the JavaScript portion but the entire source, and it fits well in Twitter’s 280-character limit. If you haven’t already checked out my 278-byte Flappy Bird, I’ll demystify this snowfall in a similar fashion.

The Code

Here’s the whole code with line breaks, tabs, and spaces for better readability.

<body onload="
    c = C.getContext('2d');
    R = Math.random;
    H = 164;
    F = [];
    f = 99;
    while (f--)
        F[f] = {
            y: R() * H,
            s: R()
        };
    setInterval(`
        c.fillRect(x = 0,0,300,H);
        for (f of F) {
        c.font = 28 * f.s + 'q a';
        f.y = f.y % H + f.s;
        c.fillText('*',x,f.y);
        x += 3;
        c.fillStyle = x > 294 ? '#000' : '#fff'
        }
    `,9)
">
<canvas id=C>
Enter fullscreen mode Exit fullscreen mode

Using a single letter for every variable is essential for writing minimal code. In addition, JavaScript minifiers often do this and should be to help reduce script size when talking web performance.

Tags and Attributes

<body onload="">
<canvas id=C>
Enter fullscreen mode Exit fullscreen mode

All the HTML we’ll ever need here is an opening <body> and <canvas> tag. Since no other elements will follow, we can omit the closing tags. As a result, we save 16 bytes (7 for </body> and 9 for </canvas>).

Since JavaScript lets us to use element IDs as variables, I used only C here for the canvas. Although attributes of one value may not require quotes, we still need them for the body’s onload because having '2d' in .getContext('2d') as a value in quotes, the browser will treat all else before and after it as separate attribute-value pairs. Then some =s will become invalid HTML syntax.

Context, Main Variables, and Snowflake Array

c = C.getContext('2d');
Enter fullscreen mode Exit fullscreen mode

As mentioned earlier, we can use the canvas ID like a variable; using document.getElementById('C') or document.querySelector('#C') would just rack up the expense.

R = Math.random;
Enter fullscreen mode Exit fullscreen mode

Since we’re using Math.random() more than once for randomizing the starting snowflake positions, we can assign R the function. Then we’d save 10 bytes per use of R()!

H = 164;
Enter fullscreen mode Exit fullscreen mode

H is the height of the canvas (150) plus enough space (14) past the bottom so that when snowflakes are moved back to the top, it doesn’t look like they magically disappear.

F = [];
f = 99;
while (f--)
    F[f] = {
        y: R() * H,
        s: R()
    };
Enter fullscreen mode Exit fullscreen mode

The last chunk of code before the setInterval() is where we generate 99 snowflakes as well as determine their starting y-positions (y) and speeds (s). For the random values, we use our byte-saving R() function. Rather than using a for loop of any kind and .push() to supply the F array, we can use fewer bytes with a while loop with a counter starting at 99. Then we can append each snowflake object using F[f]=, which is 3 bytes cheaper than F.push().

The Animation Loop

setInterval(`
    c.fillRect(x = 0,0,300,H);
    for (f of F) {
        c.font = 28 * f.s + 'q a';
        f.y = f.y % H + f.s;
        c.fillText('*',x,f.y);
        x += 3;
        c.fillStyle = x > 296 ? '#000' : '#fff'
    }
`,9)
Enter fullscreen mode Exit fullscreen mode

The setInterval() here is where all the logic and drawing happens. Although I don’t recommend doing it anyway due to security concerns, you can supply the whole function as a string or template literal.

c.fillRect(x = 0,0,300,H);
for (f of F) {
    . . .
}
Enter fullscreen mode Exit fullscreen mode

While filling the screen black by default for the first statement, we set x equal to 0 for the x-position of the first snowflake. In the for...of loop contents we’re about to explore, the x will come in handy for horizontally placing the snowflakes without using too many bytes, and we’ll see how later.

c.font = 28 * f.s + 'q a';
Enter fullscreen mode Exit fullscreen mode

In the first statement in the for...of loop, we set a maximum font size of 28q. I normally don’t use the q unit, but it happens to be great for code golfing because every other unit is at least a character longer. Then multiplying the font size by snowflake speed (f.s), we’ll get the visual depth we want. The smaller the snowflake, the further away and slower in movement it’ll appear. By the way as you’ve seen earlier in the whole chunk of code, the asterisk (*) will be the snowflake. Then as part of getting the desired shape, we can use a as a single-letter fake font family that will be rendered as the browser default. If we were to leave it out in attempt to save 2 more bytes, however, the snowflakes would wind up as dots.

f.y = f.y % H + f.s;
Enter fullscreen mode Exit fullscreen mode

The next statement moves each snowflake downward. By using f.y % H, each snowflake will be kept in its 0-164 range when its y finally exceeds H through adding f.s.

c.fillText('*',x,f.y);
x += 3;
Enter fullscreen mode Exit fullscreen mode

Once having the needed position, we throw in the snowflake itself. After that we evenly space out all 99 snowflakes as possible across the canvas by dividing the default canvas width of 300 by 99 and rounding the result, which will be 3. That’s what we use to increment x. The reason we did not give snowflakes their own x property is that obviously more bytes will be wasted if we used f.x. So that explains our use of x in the .fillRect() from earlier.

c.fillStyle = x > 294 ? '#000' : '#fff'
Enter fullscreen mode Exit fullscreen mode

The last statement in the loop determines when to switch fills. Since the last snowflake will be located at x = 294 ((x increment of 3) × (99 snowflakes) - (x increment of 3)), we can set the fill black for the background when x is finally greater than it.

setInterval(`
    . . .
`,9)
Enter fullscreen mode Exit fullscreen mode

Finally for the sake of cutting bytes, a single-digit interval of 9 milliseconds sets a comfortable animation speed.

Conclusion

There you have it, a wintery scene in about a quarter of a kilobyte! The key parts of this have been:

  • Turning a built-in JavaScript function Math.random into a single letter if using more than once
  • Changing how we may normally populate arrays
  • Not using a certain property if a pattern can be determined without it (x)
  • Tolerating defaults

Top comments (0)