Behind the Parallax Stars
Unfortunately I have been busy, so I have been unable to write this post up until now. Anyway, welcome to the second technical post on this blog. This time, we will be looking behind the Parallax Stars.
The FPS and rendering time graphs are drawn by a slightly modified version of stats.js, by mrdoob. It's original github repository can be found here, and the slightly modified version can be found here. The modifications I made allow both the FPS graph and the rendering time graphs to be displayed at once.
The interesting code that makes the stars work is found in stars.js. The function is a constructor that returns an containing everything needed to make the stars appear and animate.
this.build = function() {
this.data = [];
for (var i = 0; i < this.number; i++)
{
this.data[i] = new star();
}
if(this.depthsort == true)
{
this.data.sort(depthsort);
}
};
function star()
{
this.z = Math.random()*(0.7) + 0.3;
this.x = rand(canvas.width);
this.y = rand(canvas.height);
this.radius = Math.floor(this.z * 7);
var r, g;
r = rand(200, 255);
g = rand(200, g - 20);
this.colour = "rgb(" + r + ", " + g + ", " + 0 + ");"
this.speed = this.z * 3;
}
The build()
function contains some setup code that populates an array with stars, which are constructed by a different function, star()
. Each star is assigned an x, y, and z co-ordinate. The x and y co-ordinates determine it's position on the screen. and the z co-ordinate determines it's size and speed. The stars are also given a colour too. The function rand()
is a small utility function I sometimes use for generating random numbers:
// EXAMPLES:
// rand(256) returns a whole number between or equal to 0 and 255
// rand(100,201) returns a whole number between or equal to 100 and 200
// rand() returns a decimal between or equal to 0 and less than 1
function rand()
{
// takes 0-2 arguments (numbers)
var args = arguments, num;
if (args[0] && !args[1]) {
num = Math.floor(Math.random()*(args[0]));
} else if (args[0] && args[1]) {
num = Math.floor(Math.random()*(args[1]-args[0])) + args[0];
} else {
num = Math.random();
}
return num;
}
The array that hold all the stars is also depth sorted before we start and on each frame to keep the stars in order since stars are leaving the screen and new one coming onto the screen on every frame. The standard array .sort()
function is used, along with a custom sort function:
function depthsort(a,b) {
if(a.z < b.z)
return -1;
if(a.z > b.z)
return 1;
else
return 0;
}
The updates that happen every frame are split into two different functions. step()
deals with incrementing co-ordinates and keeping the depth sorting up to date, and render()
takes the current state and renders it on the canvas.
this.step = function() {
for (var i = 0; i < this.data.length - 1; i ++)
{
this.data[i].x += this.data[i].speed;
if(this.data[i].x > canvas.width + this.data[i].radius + 1)
{
this.data[i].x = new star();
this.data[i].x = -this.data[i].radius - 1;
if(this.depthsort == true)
{
this.data.sort(depthsort);
}
}
}
}
The this.data
array holds all of the stars. For each star, it's x value is incremented by it's speed. If this moves the star off the screen, we replace it with a new star that starts off at the left hand side. The stars are then depth sorted again to keep all the further away stars behind the stars that are (supposed to be) closer to the front.
this.render = function() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < this.data.length - 1; i ++)
{
context.globalAlpha = 1;
context.globalCompositeOperation = "destination-out";
context.beginPath();
context.arc(this.data[i].x, this.data[i].y, this.data[i].radius, 0, Math.PI*2, true);
context.closePath();
context.fill();
context.globalAlpha = this.data[i].z;
context.globalCompositeOperation = "source-over";
context.fillStyle = this.data[i].colour;
context.beginPath();
context.arc(this.data[i].x, this.data[i].y, this.data[i].radius, 0, Math.PI*2, true);
context.closePath();
context.fill();
}
}
To start off the rendering, the canvas is cleared. After that, For each star, a hole in the canvas is cleared with the destination-out
drawing mode. The reason for this is that the z co-ordinate is also used as an alpha value for each star to make it dimmer the further away it is. This gives a slightly more realistic 3d appearance. After clearing a hole, the drawing mode is rest to the default (source-over
), and the alpha and fill colours are set up according to the star's current z co-ordinates and colour respectively. Then the star is drawn. Since when this script was written we did not have a canvas drawing function to draw an ellpise, the context.arc(this.data[i].x, this.data[i].y, this.data[i].radius, 0, Math.PI*2, true);
code is used to draw circles via context.beginPath()
and context.fill()
.
Finally, One nice neat update()
function is provided that calls render()
and step()
in sequence:
this.update = function() {
this.render();
this.step();
}
That concludes this technical post! If you have any questions, please post them below and I will try my best to answer them.