Starry sky


JavaScript jQuery CSS

"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.


IMPORTANT NOTE: I've created a better solution, please read it here! Starry sky - CANVAS


I love art. And I love the web too. Internet was born with the intent of sharing information, though I think it can be way more than this. People may point me as a web-abuser making any sort of art out of HTML, CSS and JavaScript, still I love to do it.

In this tutorial we'll use basic HTML, some CSS3 rules and JavaScript. I've tested it under Chrome 31, Internet Explorer 11 and Firefox 25 under Windows. The first two are able to reproduce the web page correctly, while the third has difficulty, probably due to the virtually endless CSS3 animations (bad animations engine?).


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


The concept: a DIV containing a starry sky.

Possible solution(s):

  1. static image as background, moving foreground divs with semi-transparent background image simulating the pulsating of the background stars. PRO: easy to implement, compatible with most of the browsers. CONS: not so natural effect.
  2. dynamically generated stars with dynamic opacity CSS3 animations. PRO: more natural effect. CONS: compatible with CSS3 browsers only, needs JavaScript back-end and is not suitable with low-powered CPUs.
  3. ???

In this tutorial I'll let you see how I implemented solution (2).


1. Basic HTML & CSS

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

:::html
<div id="wrapper">
</div>

Pretty simple, huh? Everything will be done via JavaScript and a little bit of CSS. Let's see the CSS first:

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

#wrapper {
    width: 720px;
    height: 400px;
    margin: 50px auto;
    border: 1px solid rgba(255,255,255,.1);
    position: relative;
}

.star {
    background: radial-gradient(circle at center, rgba(255, 255, 255, .7) 10%, transparent 50%);
    position: absolute;
    opacity: .5;
}

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 wrapper will contain the stars (you can avoid using the wrapper if you want to be 100% filled with stars). The wrapper will have its position relative in order to have absolute positions of the stars, but relative to the wrapper. Stars will have a radial "body" and a base opacity of .5 (opacity will be the property we'll use in order to make them spark).

2. JavaScript functions

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

First of all, let's create a simple function which will return a random integer in a range. This will be useful when generating random animation rules:

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

Pretty simple. Now let's take a look to the keyframes generator. As I previously wrote, we'll use the opacity property in order to simulate the pulsation of the stars. Keyframes are CSS3 rules that define a custom animation. This is a simple example of what we're going to generate:

:::css
@keyframes example {
    0% { opacity: .7; }
    43% { opacity: .2; }
    57% { opacity: .8; }
    87% { opacity: .8; }
    100% { opacity: .7; }
}

The first (0%) and last (100%) steps will have the same value. Between them there will be n random steps with random opacity values. This is the W3C working draft style. Because it's still not in the RFC form, some browsers still adopt a custom prefix (i.e. @-webkit-keyframes), but for now we simply don't care, as we're going to output the body of the rule, not the rule itself.

:::javascript
function generateRandomSparklingFrames(steps, minOpacity, maxOpacity)
{
    var opacity = [],
        percent = [];
    for(i=0; i < steps; i++) {
        do {
            opacity[i] = Math.random().toFixed(2);
        } while (opacity[i] < minOpacity || opacity[i] > maxOpacity);
        percent[i] = randomIntBetween(1,99);
    }
    percent.sort();

    rule = "0% { opacity: " + opacity[0] + "} ";
    for(i=0; i < steps; i++) {
        rule += percent[i]+"% { opacity: " + opacity[i] + "} ";
    }
    rule += "100% { opacity: " + opacity[0] + "} ";

    return rule;
}

There are three arguments: the first is the number of steps, the second the minimum opacity (from 0 to 1), the last the maximum opacity (from 0 to 1). In this way we'll be able to refine our generation algorithm in order to fine-tune the intensity of both the pulsation and the color.

Note that there is a potential dead-lock in the do ... while statement (if you set minOpacity > maxOpacity), so take care of it.

The "percent" variable is being sort in order to have an... ordered list of steps.

Taking the previous example as output, it will be:

0% { opacity: .7; } 43% { opacity: .2; } 57% { opacity: .8; } 87% { opacity: .8; } 100% { opacity: .7; }

The last function we need in order to make our algorithm work (besides the algorithm itself) is the one that will inject the random-generated keyframes in the current style sheet.

:::javascript
function insertStarRule(name, numSteps, minOpacity, maxOpacity, animationTimeMin, animationTimeMax) {
    CSS = document.styleSheets[0]; // take the default style sheet

    if (CSS.insertRule) {
        try {
            CSS.insertRule("@-webkit-keyframes " + name + "{ " + generateRandomSparklingFrames(numSteps, minOpacity, maxOpacity) + "}", CSS.cssRules.length);
            CSS.insertRule("." + name + " { -webkit-animation: " + name + " " + randomIntBetween(animationTimeMin, animationTimeMax) + "s infinite }", CSS.cssRules.length);
        } catch (e) {}
        try {
            CSS.insertRule("@keyframes " + name + "{ " + generateRandomSparklingFrames(numSteps, minOpacity, maxOpacity) + "}", CSS.cssRules.length);
            CSS.insertRule("." + name + " {animation: " + name + " " + randomIntBetween(animationTimeMin, animationTimeMax) + "s infinite }", CSS.cssRules.length);
        } catch (e) {}
    }

    if(CSS.addRule) {
        try {
            CSS.addRule("@-webkit-keyframes " + name, generateRandomSparklingFrames(numSteps, minOpacity, maxOpacity));
            CSS.addRule("." + name, "-webkit-animation: " + name + " " + randomIntBetween(animationTimeMin, animationTimeMax) + "s infinite");
        } catch (e) {}
        try {
            CSS.addRule("@keyframes " + name, generateRandomSparklingFrames(numSteps, minOpacity, maxOpacity));
            CSS.addRule("." + name, "animation: " + name + " " + randomIntBetween(animationTimeMin, animationTimeMax) + "s infinite");
        } catch (e) {}
    }
}

Ok, I admit, this is not the best engineering work ever, it's pretty hackish, but it's because of two reasons:

  1. the StyleSheet object may have the method insertRule, addRule or both depending on the browser. This is why of the two main blocks.

  2. invalid CSS rules cause a JavaScript exception. Webkit-based browsers support the prefixed version of keyframes, while the others the W3C specification. Because inserting browser-specific code is always a bad habit (things may change or unknown browsers may not be specified in the code), I've opted for a series of try ... catch statements, eventually the right one will be inserted while the others will be ignored.

The important thing is that a couple of keyframes and class rules are injected in the style sheet. Every star will have its own class and keyframes rules, and this is exactly the CPU bottleneck: the more stars, the more computation.

Arguments are: the name of the rule, the number of steps, the minimum and maximum opacity used by the previous function, and the animation minimum and maximum duration set in the class associated with the keyframes rule.

3. JavaScript controller

Now that we've got all the functions, let's take a look at the main algorithm. Please note that I've used ZeptoJS, a lightweight jQuery-like library, for the elements selectors:

:::javascript
var w = $("#wrapper").width(),
    h = $("#wrapper").height(),
    starsCount = 1000;

// generate the CSS rules
for(n = 1; n <= starsCount; n++)
    insertStarRule("s" + n, 5, .2, .7, 8, 20);


// create the star DIVs and associate the right rule
for(i = 0; i < starsCount; i++)
{
    var size = randomIntBetween(1,6),
        x = Math.min(randomIntBetween(1,w), w-size-5),
        y = Math.min(randomIntBetween(1,h), h-size-5),
        elem = $("<div class='star'></div>");

    elem.css({"top": y, "left": x, "width": size, "height": size});
    elem.addClass("s"+randomIntBetween(1,starsCount));
    $("#wrapper").append(elem);
}

This script will generate 1000 DIVs representing the same number of stars.

The number can be changed modifying the starsCount variable.

The coordinates generation part has been adjusted (-5 pixels) in order to avoid them to be put outside the wrapper. Modifying the insertStarRule call can adjust the color intensity (3rd and 4th parameters), the sparkling speed (5th and 6th parameters). If you need more sparkling steps you can edit the 2nd parameter (even if I think the sparkling speed parameters are enought for it).

This is the full code on JSFiddle.net

What now?

This is the base star field. You can now add brighter and bigger stars. You can create stars of different colors adding custom classes. I've personally wrote a name in the sky :)

- 17th November 2013

> back