Developing HTML5 Applications, Part 2
- Introducing Animated Starry Diorama
- Playing ASD Audio Without the Audio Element
- Adapting ASD's Canvas to Cover the Browser's Client Area
Editor's Note: Be sure to read Part 1 of this 3-part series.
HTML5 is an exciting update to the HTML specification, and many developers are creating HTML5 applications. Because browser implementations of this specification are slowly maturing (it will be years before the HTML5 specification is finalized), some browsers do not implement certain HTML5 features correctly (or even implement these features). This lack of consistency in implementing features makes it difficult to create portable HTML5 applications.
I've created this three-part Developing HTML5 Applications series to address this problem. Part 1 provided an overview of HTML5, emphasizing features used by the applications presented in this article as well as Part 3 of this series. I also looked at testing these features in various browsers to determine browser support, and presented a JavaScript library whose browser-identification functions help developers write portable HTML5 applications.
Now that Part 1 is out of the way, we can focus on designing applications that detect the current browser and adapt to its HTML5 limitations. This article explores this topic in the context of Animated Starry Diorama (ASD, for short), an entertainment-oriented application whose only HTML5 dependencies are its audio and canvas elements. After introducing ASD and focusing on its design, the article presents an audio element alternative for browsers that don't support this feature, and shows you how to adapt the canvas to cover a browser's client area.
Introducing Animated Starry Diorama
Animated Starry Diorama (ASD) is an HTML5 application that presents an animated three-dimensional (3D) field of stars overlaying a galaxy image. This animation is accompanied by music from British composer Gustav Holst's The Planets suite.
You can try out ASD by pointing your browser to my website. Alternatively, you can access this application from this article's accompanying code file. Figure 1 shows you what the application looks like.
Figure 1 ASD is horizontally centered in Firefox's client area. Click to enlarge.
Application Design
ASD is composed of ASD.html and ASD.js, which collectively define this application. I organized ASD around a pair of files to simplify maintenance.
ASD.html provides the necessary HTML structure and a script for integrating this application into a browser. Listing 1 presents the contents of this HTML file.
Listing 1ASD.html
<html> <head> <title> Animated Starry Diorama </title> </head> <body> <script type="text/javascript" src="BrowserDetect.js"> </script> <script type="text/javascript"> var version; if (((version = getFirefoxVersion()) != null && cmp(version, "3.6") >= 0) || ((version = getChromeVersion()) != null && cmp(version, "6.0.472.63") >= 0) || ((version = getSafariVersion()) != null && cmp(version, "5.0.2") >= 0) || ((version = getOperaVersion()) != null && cmp(version, "10.62") >= 0)) { var WIDTH = 720; var HEIGHT = 480; document.write("<div style='text-align: center'>"); document.write("<canvas id='diorama' width='"+WIDTH+"' "+ "height='"+HEIGHT+"'><\/canvas>"); document.write("<\/div>"); document.write("<script type='text\/javascript' "+ "src='ASD.js'><\/script>"); } else { document.write("<div style='text-align: center'>"); document.write("Your browser cannot properly execute this "+ "HTML5 application."); document.write("<\/div>"); } </script> </body> </html>
ASD.html includes the BrowserDetect.js JavaScript library that I presented in Part 1 of this series. This HTML file's small script subsequently calls most of this library's functions to verify that the current browser is capable of supporting the application's HTML5 features.
If the current browser is acceptable, the script writes the necessary HTML for specifying the canvas element and including ASD.js's source code. If it's not acceptable, the script writes other HTML to inform the user that the browser cannot host the application.
The heart of the application resides in ASD.js. This file's JavaScript code consists of several global variables and functions, and inline code that initializes the application and starts it running. Listing 2 presents the contents of this JavaScript file.
Listing 2ASD.js
// ASD.js var canvas = document.getElementById("diorama"); var canvasctx = canvas.getContext('2d'); var xsize, ysize, xcenter, ycenter; var buffer; var bufferctx; var NBGSTARS = 200; var bgstarX = new Array(); var bgstarY = new Array(); var NSTARS = 200; var starX = new Array(); var starY = new Array(); var starZ = new Array(); var GRAYSCALE = 0; var RED = 1; var BLUE = 2; var color = new Array(); var shade = new Array(); var loaded = false; var image = new Image(); image.src = "galaxy.jpg"; image.onload = function() { loaded = true; } var audio; // Initialize star data, background canvas, and audio so that an audio file is // playing. function init() { xsize = WIDTH; ysize = HEIGHT; xcenter = xsize>>1; ycenter = ysize>>1; buffer = document.createElement('canvas'); buffer.width = WIDTH; buffer.height = HEIGHT; bufferctx = buffer.getContext('2d'); for (n = 0; n < NSTARS; n++) { starX[n] = -xsize+(random(xsize)<<1); starY[n] = -ysize+(random(ysize)<<1); starZ[n] = 1+random(4095); var threshold = random(100); if (threshold < 93) color[n] = GRAYSCALE; else if (threshold < 96) // fewer red stars than grayscale stars color[n] = RED; else color[n] = BLUE; // fewer blue stars than red stars } for (n = 0; n < 256; n++) shade[255-n] = n; for (n = 0; n < NBGSTARS; n++) { bgstarX[n] = random(xsize); bgstarY[n] = random(ysize); } var filename; if (getFirefoxVersion() != null || getOperaVersion() != null) filename = "neptune.ogg"; else { if (random(2) == 0) filename = "neptune.mp3"; else filename = "saturn.mp3"; } audio = new Audio(); audio.src = "http://tutortutor.ca/software/ASD/"+filename; audio.play(); } // Return a randomly generated integer ranging from 0 through limit-1. function random(limit) { return Math.floor(limit*Math.random()); } // Render the next frame of animation. function renderFrame() { bufferctx.fillStyle = "RGB(0, 0, 0)"; bufferctx.fillRect(0, 0, WIDTH, HEIGHT); bufferctx.fillStyle = "RGB(64, 64, 64)"; for (n = 0; n < NBGSTARS; n++) { bufferctx.beginPath(); bufferctx.arc(bgstarX[n], bgstarY[n], 1, 0, Math.PI*2, true); bufferctx.fill(); } if (loaded) bufferctx.drawImage(image, (WIDTH-image.width)>>1, (HEIGHT-image.height)>>1); for (n = 0; n < NSTARS; n++) { starZ[n] -= 30; if (starZ[n] < 1) { starX[n] = -xsize+(random(xsize)<<1); starY[n] = -ysize+(random(ysize)<<1); starZ[n] = 4095; } var z = starZ[n]; var x = (starX[n]<<9)/z+xcenter; var y = (starY[n]<<9)/z+ycenter; var radius = (4095-z)/1364; // radius ranges from 0 through 3. bufferctx.beginPath(); bufferctx.arc(x, y, radius, 0, Math.PI*2, true); var fs = "RGB("; switch (color[n]) { case GRAYSCALE: fs += shade[z>>4]+","+shade[z>>4]+","+shade[z>>4]+")"; break; case RED : fs += shade[z>>4]+",0,0)"; break; case BLUE : fs += "0,0,"+shade[z>>4]+")"; } bufferctx.fillStyle = fs; bufferctx.fill(); } canvasctx.drawImage(buffer, 0, 0); } // Initialize the application and specify that renderFrame() is to be called // every 5 milliseconds. init(); setInterval(renderFrame, 5);
ASD.js first declares several global variables. Although it's typically not a good idea to declare multiple global variables, which can lead to name conflicts when multiple script files are included, this isn't a problem for ASD because this is the only file that declares global variables.
The following global variables are declared:
- canvas stores a reference to the canvas element created in ASD.html. This variable is present only for convenience. I could have avoided the canvas intermediary and assigned the canvas's 2D context to canvasctx via var canvasctx = document.getElementById("diorama").getContext('2d');.
- canvasctx stores a reference to the canvas's 2D drawing context so that graphics can be rendered on the canvas.
- xsize stores the width of the canvas. It's useful for determining the horizontal region in which stars are initially positioned.
- ysize stores the height of the canvas. It's useful for determining the vertical region in which stars are initially positioned.
- xcenter stores the horizontal coordinate of the canvas's center point. It's useful when converting from 3D coordinates to 2D coordinates (as explained later in this article).
- ycenter stores the vertical coordinate of the canvas's center point. It's useful when converting from 3D coordinates to 2D coordinates (as explained later in this article).
- buffer stores a reference to a background canvas element for use in rendering a scene to avoid star flicker.
- bufferctx stores a reference to the background canvas's 2D drawing context.
- NBGSTARS stores the number of background stars to draw. These stars are rendered in a dark shade of gray to appear farther away. I've capitalized this global variable name to make it appear like a constant.
- bgstarX stores an array of horizontal coordinates for the background stars.
- bgstarY stores an array of vertical coordinates for the background stars.
- NSTARS stores the number of foreground stars to draw and animate.
- starX stores an array of horizontal coordinates for the foreground stars.
- starY stores an array of vertical coordinates for the foreground stars.
- starZ stores an array of Z coordinates for the foreground stars.
- GRAYSCALE stores a code that indicates that a star is to be animated in various shades of gray from black to white.
- RED stores a code that indicates that a star is to be animated in various shades of red from black to bright red.
- BLUE stores a code that indicates that a star is to be animated in various shades of blue from black to bright blue.
- color stores an array of color codes for the foreground stars.
- shade stores an array of shade codes that are shared by all foreground stars. These codes help determine the current color of each foreground star.
- loaded stores a Boolean true value when the background galaxy image is loaded; this variable is initially set to false.
- image stores a reference to a JavaScript Image object that is used to load the background galaxy image. Image loading takes place when the image's filename ("galaxy.jpg") is assigned to Image's src property. When the image finishes loading, the function assigned to Image's onload property is called and assigns true to loaded.
- audio stores a reference to an HTML5 Audio object that is used to play music to accompany the animation.
ASD.js next declares several functions, starting with init(). This function contains the necessary code for initializing the canvas, selecting suitable music to play, and starting the music. The music selection takes into account that Firefox and Opera play only OGG files.
init() creates the background canvas element by calling document.createElement('canvas'). It subsequently assigns, to the background canvas's width and height properties, the same width and height as the canvas element.
The random(limit) function is a convenience. This function is called from both init() and renderFrame(), and returns an integer that ranges from 0 through one less than limit.
renderFrame() renders the next frame of animation, using a background canvas to avoid flicker. After clearing the background canvas to black, it renders the background stars, followed by the galaxy image, followed by the foreground stars onto the background canvas.
The image is rendered by calling the background context's drawImage(Image, int, int) method. Each star is rendered by calling a combination of the context's beginPath(), arc(x, y, radius, startAngle, endAngle, anticlockwise), and fill() methods:
- beginPath() starts a path for rendering a shapeall canvas shapes apart from rectangles are built from paths.
- arc(x, y, radius, startAngle, endAngle, anticlockwise) describes an arc that's anchored at (x, y), has a radius of radius pixels, and is drawn from startAngle to endAngle in a clockwise direction when anticlockwise is false or an anticlockwise direction when anticlockwise is true.
- fill() renders a filled shape (call stroke() when an outlined shape is desired).
Once it finishes rendering the background canvas, renderFrame() must copy this canvas's content onto the visible canvas. This function accomplishes this task by executing canvasctx.drawImage(buffer, 0, 0);.
After the functions have been declared, ASD.js executes init() to initialize the application and setInterval(renderFrame, 5) to repeatedly execute renderFrame() every five milliseconds.
The 3D Challenge
ASD is an example of a diorama (a 3D representation of a scene, either full-size or miniature) because it generates a 3D field of stars. Although implementing 3D can be complicated, ASD's 3D implementation is not that hard to understand.
Before examining ASD's 3D-oriented code, let's review some 3D basics. First, a point in 3D space is represented by x, y, and z coordinates. It's common to locate these points according to the following 3D coordinate system:
- An X axis that corresponds to the screen's horizontal axis: The left half of the screen's horizontal axis represents negative values and the right half represents positive values.
- A Y axis that corresponds to the screen's vertical axis: The top half of the screen's vertical axis represents positive values and the bottom half represents negative valuesin ASD, the bottom half represents positive values to avoid an extra calculation (stars look the same when plotted upside down).
- A Z axis that is perpendicular to the screen and passes through its center point: The Z axis starts at 0 (the Z location of the viewer) and builds into the screen through increasing positive values.
ASD depends upon a technique called perspective projection to display a 3D point on a 2D screen. The idea is to move a 3D point closer to the vanishing point (an imaginary point at the center of the screen where nothing is visible) as the z value becomes move positive.
We achieve perspective by dividing a 3D point's x and y values by its z value and plotting the pixel at the resulting (x, y) location. The x and y values are multiplied by a constant before dividing by z to avoid an exaggerated perspective effect caused by small differences in z values.
Following the division, the screen's center point location is added to the resulting x and y values to derive the actual location for plotting the point. The following expressions show you how to perform this perspective projection in JavaScript:
var xScreen = x*vpd/z+xScreenCenter; // vpd is a constant representing the var yScreen = y*vpd/z+yScreenCenter; // distance to the view plane
vpd represents the distance from 3D center point (0, 0, 0)the location of the viewerto the view plane (the plane on which 3D objects, such as ASD's stars, are projected). The view plane represents the screen in 3D space.
Points in front of the view plane (z is less than vpd) are not visible to the user and should be clipped so that they are not seendisplaying these points could lead to strange effects. This consideration results in the following enhancement to the previous code fragment:
if (z >= vpd) { var xScreen = x*vpd/z+xScreenCenter; var yScreen = y*vpd/z+yScreenCenter; }
We can enhance the 3D effect by brightening points as they move from a back-clipping plane to the view plane. Points whose z coordinates put them beyond the back-clipping plane are invisible and not displayed, and the following code fragment takes this into account:
if (z >= vpd && z <= bcpd) // bcpd is a constant (often 4095) representing the { // distance from (0, 0, 0) to the back-clipping plane var xScreen = x*vpd/z+xScreenCenter; var yScreen = y*vpd/z+yScreenCenter; }
Now that you have sufficient information to understand ASD's 3D implementation, let's explore that implementation, beginning with the following code fragment from the init() function:
starX[n] = -xsize+(random(xsize)<<1); starY[n] = -ysize+(random(ysize)<<1); starZ[n] = 1+random(4095);
This code fragment assigns x, y, and z coordinate values to star n's starX, starY, and starZ array entries: x ranges from -xsize to xsize, y ranges from -ysize to ysize, and z ranges from 1 to 4095.
I chose these ranges for x and y so that, if positioned on the back-clipping plane, each star would appear within a small region of this plane centered about the middle of the screen. Of course, this assumes that z is always set to 4095.
Instead of assigning 4095 to starZ[n], I assign a randomly selected number from 1 through 4095 to this array entry. I don't include 0 in this range because it opens the possibility of division by 0, although I safeguard against this possibility later in the code.
I assign a random number to avoid a visual anomaly where all stars initially appear in a rectangular grid, move toward the view plane in unison, and disappear in unison with no additional stars appearing for a brief moment. The resulting animation looks horrible.
The second code fragment (from the renderFrame() function) is concerned with decrementing each star's value (by the same amount) so that the star moves towards the view plane, and moving the star to a new position on the back-clipping plane if its z coordinate goes out of range:
starZ[n] -= 30; if (starZ[n] < 1) { starX[n] = -xsize+(random(xsize)<<1); starY[n] = -ysize+(random(ysize)<<1); starZ[n] = 4095; }
Ideally, I should be clipping against the view plane, so the if statement's expression should read starZ[n] < 512. However, I prefer to clip against 1 (in front of the view plane) to prevent stars that start out close to the center of the screen from disappearing prematurely.
Assuming that starZ[n] is greater than or equal to 1, I apply the perspective projection via the following code fragment (also from renderFrame()):
var z = starZ[n]; var x = (starX[n]<<9)/z+xcenter; var y = (starY[n]<<9)/z+ycenter;
This code fragment assigns starZ[n] to z for convenience. It multiplies each of starX[n] and starY[n] by 512 to ensure that the perspective is not exaggeratedall stars would be animated in a small area around the center the screen if I did not multiply by 512.
This is pretty much it regarding ASD's 3D implementation. However, I also use the value assigned to z to calculate a star's radius (initially 0 and ranging to 3), and to determine the shade of gray, red, or blue in which to color the star.