Skip to content
August 3, 2012 / geeksretreat

HTML5 Canvas – An egg timer (hourglass) with animated falling sand

So, I had the bright idea of producing an animated egg timer with falling sand using HTML5’s Canvas. I considered this to be a simple task that wouldn’t take long. The particular project turned out to be the opposite! At one point I nearly gave up due to implementing a algorithm that assessed each sand particle one-by-one, and calculated its movement based on the movement of all the other sand particles. I ended up throwing that code away and starting again.
Egg Timer coded in JavaScript and HTML5
The final product functions to an acceptable level; however, I am not happy with the final product because I decided to compromise; thus, I would re-factor parts of the code.  But, in order to complete this post I decided to leave this task for another day.The final product is displayed on the left.  A working version can be viewed here.
The folllowing HTML is used to render the page:
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>Sand Timer</title>
		<script src='sand_timer.js'></script>
	</head>
	<body onload='init();'>
		<canvas id="sand" width="400" height="320">Canvas not supported</canvas>
	</body>
</html>
The HTML is straight forward. The items of interest are the canvas element and the ‘onload’ event for the body. For those who are not familiar with HTML let me explain how we draw the timer onto the canvas. When any HTML page loads the browser will run the JavaScript code attached to the onload event of the body tag e.g.
<body onload='init();'>
In order to understand how the ‘init’ function works we need to understand how the browsers locates it. The browser will load any JavaScript code included in a script tag prior to displaying the page to the user e.g.
<script type="text/javascript" src="sand_timer.js"></script>
Therefore, the content of the script “sand_timer.js” will be loaded, and therefore, the function called ‘init’ will be available. If you were to look inside the aforementioned script you will be able to see the code e.g.
function init() {

	// Grab the canvas element
	canvas = document.getElementById('sand');

	// Canvas supported?
	if (canvas.getContext('2d')) {
		ctx = canvas.getContext('2d');

		// Load the timer
		sand_timer = new Image();
		sand_timer.src = 'hourglass.jpg';
		sand_timer.onload = imgLoaded;

	} else {
		alert("Canvas not supported!");
	}
}
The ‘init’ function first obtains a handle to the canvas element we included in the HTML. It then verifies whether the browser supports the HTML5 canvas implementation. If so, a new image object is created and its source it set to the image ‘hourglass.jpg’. We then ask the browser to call the ‘imgLoad’ function after the image has been loaded. This function contains the following:

function imgLoaded() {

	initBoundary();
	initSand();

	 animate();
}

The function above makes a call to two functions called ‘initBoundary’ and ‘initSand’. It then enters the main loop – the function call ‘animate’. The initialisation functions create two arrays which are fundamental to the animation. The first function create the boundary array. This array holds the x and y screen coordinates of each edge of the egg timer. The function contains the following:

function initBoundary() {

    leftBoundary = [
        [142, 51], [140, 54], [140, 59], [139, 64], [139, 69], [139, 74], [139, 79], [140, 84], [141, 89], [142, 94],
        [144, 99], [146, 104], [150, 109], [153, 114], [158, 119], [160, 122], [162, 124], [165, 127], [167, 129], [169, 131],
        [173, 134], [176, 136], [180, 139], [183, 141], [186, 144], [188, 146], [190, 149], [191, 150], [193, 154], [195, 159],
        // Middle
        [195, 164], [193, 169], [188, 174], [183, 179], [177, 184], [170, 189], [164, 194], [158, 199], [154, 204], [150, 209],
        [147, 214], [145, 219], [142, 224], [141, 229], [140, 234], [139, 239], [139, 244], [139, 249], [139, 254], [139, 259],
        [140, 264], [142, 269]
    ];

    rightBoundary = [
        [259, 51], [260, 54], [261, 59], [262, 64], [262, 69], [262, 74], [261, 79], [260, 84], [258, 89], [257, 94],
        [255, 99], [253, 104], [251, 109], [247, 114], [243, 119], [239, 124], [237, 126], [234, 129], [229, 134], [226, 136], [224, 138],
        [222, 139], [219, 141], [216, 144], [215, 144], [212, 147], [210, 149], [208, 151], [206, 154], [205, 159],
        // Middle
        [205, 164], [207, 169], [211, 174], [217, 179], [225, 184], [231, 189], [236, 194], [241, 199], [245, 204], [249, 209], [252, 214],
        [255, 219], [257, 224], [259, 229], [260, 234], [261, 239], [261, 244], [261, 249], [260, 254], [260, 259], [259, 264], [257, 269]
    ];
}
The above looks daunting; however, seen visually it all becomes clear. If one was to draw a line between the points in each array we would end up with the following:

Edge of egg timer

The second initialisation function ‘initSand’ contains the following:
function initSand() {

    var iYCounter = 0, iXMin = 0, iXMax = 0;

    for ( iYCounter = floor; iYCounter >= ceiling; iYCounter--) {

        iXMin = findXAtY(iYCounter, leftBoundary);
        iXMax = findXAtY(iYCounter, rightBoundary);

        gridBounds[iYCounter] = [iXMin, iXMax];
        grid[iYCounter] = [iXMax - iXMin];

        if (iYCounter > ceiling + 50 && iYCounter <= 159) {

            initSandForRow(iXMin, iXMax, iYCounter);

        } else {

            initEmptyRow(iXMin, iXMax, iYCounter);

        }
    }
}
The above function creates a grid of cells in which the sand particles can occupy. Starting from the top of the egg timer we assess each row, obtaining the minimum and maximum x coordinates from the boundary array. For each cell along the x-axis we create an object that either holds a blank cell (initEmptyRow) or a sand particle (initSandForRow). In its default state sand occupies the row from the ceiling + 50 pixels to and including row 159 (middle point). If one was to render the hourglass onto the canvas and with the sand in this position we would see the following:

Hourglass in suspended animation

Revisiting the ‘imgLoaded’ function the last step within this function is the call to the main animation loop e.g
	 animate();
The ‘animation’ function contains the following:
function animate() {

    if (drawSandParticles() === false) {
        setTimeout(function() {
            animate();
        }, 100);
    }
}
The animation function was quite tricky to produce. Emulating the natural flow of sand under the force of gravity took several iterations and a few algorithms were created and later replaced as the code matured. The final result isn’t perfect and one feels it could be improved. However, as a proof of concept it functions. The core of the animation occurs within ‘drawSandParticles’ which is iteratively called until the sand has settled at the bottom. This functions contains:(
function drawSandParticles() {

    var iYCounter = 0, 
        iXCounter = 0, 
        bComplete = false, 
        iStepCounter = 4;

    drawBackground();

    // Adjust the sand by x number of steps
    while (iStepCounter >= 0) {

        // Allow any particle to move down
        bHasMoved = applyGravity(0, 1);

        // Allow any particle to move left/right
        bHasMovedRight = applyGravity(1, 1);
        bHasMovedLeft = applyGravity(-1, 1);

        iStepCounter--;

        if (bHasMoved === false && bHasMovedRight === false && bHasMoved === false) {
            iNilMoveCounter++;
        }
    }

    cleanUp();

    // Draw the sand in the new location
    for ( iYCounter = floor; iYCounter >= ceiling; iYCounter--) {

        iXMin = gridBounds[iYCounter][0];
        iXMax = gridBounds[iYCounter][1];

        // Iterate from left to right within the boundaries
        for ( iXCounter = iXMin; iXCounter <= iXMax; iXCounter++) {

            drawSandParticle(grid[iYCounter][iXCounter]);

        }
    }

    // Draw the boundary line
    drawSandBoundary();

    return (iNilMoveCounter > 250);
}
This function can be split into four sections. The first of which is to draw the background image. The code for this is within the aptly function ‘drawBackground’. This function clear the canvas and draws the image loaded earlier onto the canvas e.g.

The second section handles the movement of each sand particle. Specifically the function attempts to move each sand particle according to the laws of gravity. Starting at the lowest row we check each sand particle from left to right, row by row, determining whether the sand can move a) down one on row, or, b) whether the sand can naturally fall left or right. The sand algorithm is used for each movement, with the target cell passed as a parameter to the generic function. This function is called ‘applyGravity’ and includes the following:
function applyGravity(iXAdjust, iYAdjust) {

    var iYCounter = false, iXCounter = false, sand = null, cell = null, iXMin = null, iXMax = null, bRowHasSand = false, bHasMoved = false;

    if (iFirstRowOfSand >= floor) {
        iFirstRowOfSand = floor - 1;
    }

    for ( iYCounter = iFirstRowOfSand; iYCounter >= ceiling; iYCounter--) {

        iXMin = gridBounds[iYCounter + iYAdjust][0];
        iXMax = gridBounds[iYCounter + iYAdjust][1];

        bRowHasSand = false;

        for ( iXCounter = iXMin; iXCounter <= iXMax; iXCounter++) {

            sand = grid[iYCounter][iXCounter];
            cell = grid[iYCounter + iYAdjust][iXCounter + iXAdjust];

            bHasMoved = false;

            if (sand !== undefined && sand.occupied === true) {

                if (cell !== undefined && cell.occupied === false) {

                    // Sand in target cell?
                    if (sand.x >= iXMin && sand.x <= iXMax) {

                        //Swap the sand cells
                        grid[iYCounter + iYAdjust][iXCounter + iXAdjust].colour = sand.colour;
                        grid[iYCounter + iYAdjust][iXCounter + iXAdjust].occupied = true;

                        grid[iYCounter][iXCounter].colour = 'white';
                        grid[iYCounter][iXCounter].occupied = false;

                        bHasMoved = true;
                    }
                }

                bRowHasSand = true;
            }
        }

        //Exit the loop if the row has no sand
        if (bRowHasSand === false) {
            iYCounter = ceiling - 1;
        }
    }

    iFirstRowOfSand++;

    return bHasMoved;
}
The movement of each sand particle is permitted if the target cell is empty. If a particle can be moved the objects attributes such as its x and y position are swapped with the target cell e.g.
// Sand in target cell?
if (sand.x >= iXMin && sand.x <= iXMax) {
    
    //Swap the sand cells
    grid[iYCounter + iYAdjust][iXCounter + iXAdjust].colour = sand.colour;
    grid[iYCounter + iYAdjust][iXCounter + iXAdjust].occupied = true;

    grid[iYCounter][iXCounter].colour = 'white';
    grid[iYCounter][iXCounter].occupied = false;

    bHasMoved = true;
}
This loop is repeated four times to hasten the effect of falling sand. I feel this part could be tweaked to obtain a smoother movement of sand; but, for now I’ve opted to settle for the not-so-smooth effect :-). The third step of the ‘drawSandParticles’ function is to iterate around each cell and draw it onto the canvas e.g.
    // Draw the sand in the new location
    for ( iYCounter = floor; iYCounter >= ceiling; iYCounter--) {

        iXMin = gridBounds[iYCounter][0];
        iXMax = gridBounds[iYCounter][1];

        // Iterate from left to right within the boundaries
        for ( iXCounter = iXMin; iXCounter <= iXMax; iXCounter++) {

            drawSandParticle(grid[iYCounter][iXCounter]);

        }
    }
And the function that actually draws each particle ‘drawSandParticle':
function drawSandParticle(current) {

    if (current !== undefined) {

        ctx.strokeStyle = current.colour;
        ctx.fillStyle = current.colour;
        ctx.fillRect(current.x, current.y, 1, 1);

    }
}
This function used the canvas’ context (ctx) to draw a rectangle of 1 pixel using the colour associate with the current sand particle. The fourth step of the main animation loop is to redraw the boundary which simply keeps the edges clean. Finally, the function returns a true or false value to the calling interface. This value is driven by the number of nil movements. Every time the movement loop fails to move a partcile the number of nil movement is increased. After a given number of nil movements has passed we can safely say the sand a stopped moving, or, in other words the sand is in a pile at the bottom of the hourglass. The following depicts the hourglass at staged intervals of the animation:

4 stages of the egg timer

That concludes one’s experiment with HTML5′s canvas. I hope this helps someone. All the code above may be taken and used for anything – credit not required. A working version of this project can be viewed here. The source code for this project can be obtained from my Git Hub repository here. Full credit for the original digital image goes to this site for providing the PSD.
About these ads

3 Comments

Leave a Comment
  1. Jagriti / May 22 2013 11:45

    I liked the way of showing hour glass animation. But I need to show this animation until the estimated time has reached. O simply show this animation for a duration to show time left for completion. Simply increasing the count of animation does not work for me. It becomes too jaggy.

    • geeksretreat / May 22 2013 18:29

      I considered this exact point why doing this and avoided implementing the time/animation ratio. My theory is that the relation between the amount of sand particles and the width of the gap and the effect of gravity can be used in a mathematical expression to hit a nicer balance between the animation time and required estimated time. I’d love to see this implemented if anyone can help.

Trackbacks

  1. HTML5 Canvas – An egg timer (hourglass) with animated falling sand « Geek’s Retreat « Sutoprise Avenue, A SutoCom Source

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 162 other followers

%d bloggers like this: