Starry sky - CANVAS


HTML5 Canvas Javascript jQuery

"Twinkle twinkle little star"... how many times have you ever looked up to the sky during the night, dreaming about the endless stars out there? In this tutorial I'll explain you how I made a random-generated starry sky.

Maybe you've already read my previous article, Starry sky. There were mainly two problems: it was compatible with Internet Explorer 11 only (IE series) and not with Firefox. Also, it was a CPU sinker, thus making it impossible to view via mobile devices. If you haven't already, please read my previous article, then continue reading this one.


IMPORTANT NOTE: this work is under the Creative Commons Attribution 3.0 Unported licence


The concept: a CANVAS containing a starry sky.

Pros: compatible with all the major browsers, including Internet Explorer 9+

Cons: not compatible with non-HTML5 enabled browsers


Step 1: basic HTML & CSS

This is all the HTML inside the <body> tag:

:::html
<canvas id="star_field" width="1280" height="720"></canvas>

Differently from the previous implementation, we're going to use the canvas element. The main pro using this instead of a div is that we will draw in it instead of creating N childs (one per star). Because of this we will be able to virtually draw as many stars as we want (5000+ stars? Always one object).

Also note that we have specified the canvas size directly in HTML: if we do it in CSS, the resulting drawing will be streched.

:::css
body {
    background: black;
    text-align: center;
}

#star_field {
    margin: 50px auto;
    border: 1px solid rgba(255,255,255,.1);
}

The background will be black and it will have the text aligned center in order to put the wrapper in the middle of the page. The canvas #star_field will contain the stars. All the stars styling will be done in javascript using the canvas APIs.

2. JavaScript functions

Now that we've got a base field on which to cultivate our sparkling garden, let's plant the seeds!

window.requestAnimFrame

First of all, we need to understand how a canvas element works: it's not a dynamic "paper" on which you can program animations or items moving. It's more like... well... a canvas. You can paint, you can erase. The result will always be a single image. If you want (and we do want in this case) to animate something, you have to "book" the render engine for a change.

Browsers come in help to us with a function (called differently depending on the browser) which executes some code before the next canvas render. The following code simply uniforms this function's name (we'll use it later):

:::javascript
/*
    Credits:
    http://www.html5canvastutorials.com/advanced/html5-canvas-animation-stage/
*/
window.requestAnimFrame = (function(callback) {
    return window.requestAnimationFrame || window.webkitRequestAnimationFrame
        || window.mozRequestAnimationFrame || window.oRequestAnimationFrame
        || window.msRequestAnimationFrame || function(callback) {
            window.setTimeout(callback, 1000 / 30);
        };
})();

math functions

Now we're going to implement two very simple math functions, the first one in order to get a random integer between a range (useful to get a random index of arrays, for example), the second one in order to get a float around a number (+ or - 0.1, see stars brightness change).

:::javascript
function randomInt(a, b) {
    return Math.floor(Math.random()*(b-a+1)+a);
}

function randomFloatAround(num) {
    var plusminus = randomInt(0, 1000) % 2, // is it odd? 0/1 (i.e. false / true)
        val = num;
    if(plusminus)
        val += 0.1;
    else
        val -= 0.1;
    return parseFloat(val.toFixed(1)); // .toFixed returns a string, we need a float
}

some variables

:::javascript
var canvas = document.getElementById("star_field"), // the canvas in the DOM
    context = canvas.getContext('2d'), // get the 2D context
    sizes = ['micro', 'mini', 'medium', 'big', 'max'], // array of possible star sizes
    elements = [], // the stars (see later)
    max_bright = 1, // maximum star brightness (i.e. opacity) (0-1)
    min_bright = .2; // minimum star brightness (i.e. opacity) (0-1)

Note that these are all global variables. Because of this, in order to preserve the global space clear, we're going to embed all the script into an anonymous function later. In the following functions we'll use these global variables without the need of referencing them in the functions' arguments.

function star

The star function draws the star in the canvas. If we just wanted to draw a static star field, it would have been just a little bit less "complex": draw the star, end. Because we're using a canvas, we first need to erase the space where the star previously was (the actual pixels) and then re-draw it. Otherwise the two brightnesses would sum making the stars brighter and brighter, instead of sparking.

:::javascript
function star(x, y, size, alpha) {
    var radius = 0;
    switch(size) {
        case 'micro':
            radius = 0.2;
            break;
        case 'mini':
            radius = 0.4;
            break;
        case 'medium':
            radius = 0.6;
            break;
        case 'big':
            radius = 0.8;
            break;
        case 'max':
            radius = 1.0;
            break;
    }

    gradient = context.createRadialGradient(x, y, 0, x + radius, y + radius, radius * 2);
    gradient.addColorStop(0, 'rgba(255, 255, 255, ' + alpha + ')');
    gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');

    /* clear background pixels */
    context.beginPath();
    context.clearRect(x - radius - 1, y - radius - 1, radius * 2 + 2, radius * 2 + 2);
    context.closePath();

    /* draw star */
    context.beginPath();
    context.arc(x,y,radius,0,2*Math.PI);
    context.fillStyle = gradient;
    context.fill();

    return {
        'x': x,
        'y': y,
        'size': size,
        'alpha': alpha
    };
}

First of all we check for the given size (a string) and then decide the radius of the star (which will be a filled circle btw). You can change the radius definitions if you want, these are just the dimensions that look OK on my screen (you may want to make them bigger if you want to see them clearer on higher DPI displays).

Then we create a canvas radial gradient (parameters are: coord_x, coord_y, radius of the starting circle, ending coord_x radius, ending coord_y radius, the radius of the ending circle). Eventually we create two color-stops, i.e. the starting radius color and the ending radius color (from white (with alpha as defined in the parameter) to transparent).

Then we proceed with the drawing part: we need to clear the area before, as previously written. The canvas API doesn't have an arc (circle) clear function, so we're going to use a rectangular one. In order to avoid halo effects, we're going to erase a rectangular a little bit bigger than the original circle.

Then we draw the circle. Circles are drown using the context's arc method. You can create... arcs, but even circles if you tell the method to write a 360° arc. You can fill or stroke them. Parameters are: coord_x, coord_y, radius, angle_in_radiants. Because we want stars and not circles, we're going to fill the arc, accordingly styled with the previously generated gradient.

We eventually return an object. Why? Remember the fact that canvas stores pixels? Yep, it doesn't store objects. So we have to do it by ourselves. We save the position, the size and the alpha in order to be able to overwrite that single star when we want. The only thing we'll change is the alpha (we'll see it in a second).

function generate

:::javascript
function generate(starsCount, opacity) {
    for(var i = 0; i < starsCount; i++) {
        var x = randomInt(2, canvas.offsetWidth-2),
            y = randomInt(2, canvas.offsetHeight-2),
            size = sizes[randomInt(0, sizes.length-1)];

        elements.push(star(x, y, size, opacity));
    }
}

This function will actually generate the star field using the star one. As previously written, sizes, elements and canvas are three global variables. The second is an array containing each star reference. We'll use it again in order to create the sparkling effect.

function spark

:::javascript
function spark(numberOfStarsToAnimate) {
    for(var i = 0; i < numberOfStarsToAnimate; i++) {
        var id = randomInt(0, elements.length - 1),
            obj = elements[id],
            newAlpha = obj.alpha;
        do {
            newAlpha = randomFloatAround(obj.alpha);
        } while(newAlpha < min_bright || newAlpha > max_bright)

        elements[id] = star(obj.x, obj.y, obj.size, newAlpha);
    }

    requestAnimFrame(function() {
        spark(numberOfStarsToAnimate);
    });
}

And this is the last function. It is responsible for animating the stars. It takes N random elements from the elements global variable and alters their alpha with an offset of +-0.1, then it saves the new object in the array. Eventually it asks the browser to execute itself (it's thus a recursive function) before the next canvas rendering. FPS is around 27, thus animating roughtly 1350 stars per second. And it's not stressing the CPU! Fantastic!

logic calls

:::javascript
generate(3000, .5);
spark(30);

Generate, spark. Go!

3. Conclusion

Now, I think this is a better-engineered solution, compared to the previous DIV one. We're drawing and animating 3000 stars with little to no effort, compared to the previously 1000 stars and struggling to keep the frame rate high.

4. The code

Here on JSFiddle I've put the entire project. I've already anonymised the entire thing, please ask me in the comments if you haven't understood the process. The only thing I've changed is the canvas size. About the performance, I run a test with 10000+ stars with no problems at all!

5. What now?

I've personally created a function which draws stars simply clicking on the canvas. Guess what? I've created a special version of this script in order to write a sparkling name in the nighty sky - write your name, dump the variable where the clicked points were stored, save them in an array in the script and then execute star() for each element.

In order to get the best result I'd suggest you to include these statically-generated stars in the elements array, too.

In order to slow the sparkling effect, just turn down the spark's parameter (i.e. to a value of 15 or even lower).

- 8th December 2013

> back