Because it works on a pixel-by-pixel basis, canvas is perfectly suited to making extremely detailed images algorithmically. A good example is a starfield background; a very simple one can be built based by merely deciding if a canvas random pixel is white or not.

Unlike most other articles on The New Code, the goal of this piece is not to show a complete example, but to improve the code in incremental steps. This is in contrast to the approach of many of my students, who often seek a single, “perfect” solution rather than taking the time to build from something simple (but well-structured) into a more complex, nuanced production.

In The Beginning

Let’s say we have a 750 × 500 pixel <canvas> element with an id:

<canvas id="starfield" width="750" height="500"></canvas>

And some CSS to fill it with black:

canvas {
    background: #111;
}

To start, we’ll identify the canvas, tell JavaScript what context we are drawing in, and determine the total number of stars:

var canvas = document.getElementById("starfield"),
context = canvas.getContext("2d"),
stars = 200;

To create the stars themselves, we’ll create a simple loop that draws 1 pixel × 1 pixel squares randomly within the limits of the canvas:

for (var i = 0; i < stars; i++) {
    x = Math.random() * canvas.offsetWidth;
    y = Math.random() * canvas.offsetHeight;
   context.fillStyle = "white";
    context.fillRect(x,y,1,1);
}

The result:

A simple star field, with small square white stars

That’s not a bad start, but there are a few problems: the stars are fairly obviously square when viewed closely, all the same brightness, and quite small. Let’s address all of those issues at the same time by drawing circles for the stars; each circle will have a random radius.

for (var i = 0; i < stars; i++) {
    var x = Math.random() * canvas.offsetWidth;
    y = Math.random() * canvas.offsetHeight,
    radius = Math.random() * 1.2;
    context.beginPath();
    context.arc(x, y, radius, 0, 360);
    context.fillStyle = "hsla(200,100%,50%,0.8)";
    context.fill();
}

The starfield now looks like this:

An improved star field, with small blue circles for stars

That’s better, but all our stars are blue-shifted due to the hsla color. We want a range of color.

If you’re familiar with HSL color, you should know that anything with a luminosity of 100% will appear white, no matter what the hue and saturation. So if we randomize hue and saturation but keep luminosity fairly high, we’ll get hints of color, but the stars will be predominantly white.

Our JavaScript becomes:

function getRandom(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }           
var canvas = document.getElementById("starfield"),
context = canvas.getContext("2d"),
stars = 500,
colorrange = [0,60,240];
for (var i = 0; i < stars; i++) {
    var x = Math.random() * canvas.offsetWidth;
    y = Math.random() * canvas.offsetHeight,
    radius = Math.random() * 1.2,
    hue = colorrange[getRandom(0,colorrange.length - 1)],
    sat = getRandom(50,100);
    context.beginPath();
    context.arc(x, y, radius, 0, 360);
    context.fillStyle = "hsl(" + hue + ", " + sat + "%, 88%)";
    context.fill();
}

The getRandom function creates a random integer between two numbers (inclusive): with the hue variable, it randomly selects a value of 0, 60, or 240 (avoiding green), and for saturation, a number from 50 to 100.

To quickly make the <canvas> responsive, add a percentage width in CSS:

canvas {
    width: 100%;
    height: auto;
    background: #111;
}

The Fine-Grained Nature of Reality

The final result, shown at the top of this article, is pretty good, but does lack a few features:

  • dust obscures a significant portion of the night sky: in our own galaxy, the most visible aspect of this phenomenon is the Great Rift, spanning across the middle of the Milky Way. These “clouds” should be rendered as well; I’ll reserve that for a future article.
  • it would be nice to generate dense clusters of stars, such as nebulae and the central bar of our own Milky Way. Again, I’ll leave that for a more advanced article.
  • Finally, it would be good to have the script draw stars appropriate for the current viewport*: when the <canvas> is small, it tends to be overcrowded with 200 stars, while it looks a little large and scattered on larger screens. The <canvas> will be redrawn on viewport resize: that’s just its nature when it’s made responsive in this way - but it could still be improved.

As written, the starfield could make an excellent random background for a page: you’d just have to use absolute positioning, to ensure that it was always at the “back” of subsequent elements.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/QjaXKJ