reptile7's JavaScript blog
Monday, November 26, 2012
Snow Windows
Blog Entry #271

We continue today our analysis of Altan d.o.o.'s falling snow scripts. Altan's original snow script was written in 1999; Altan updated his script in 2005, at a time when the current versions of Internet Explorer and Netscape were IE 6 and Netscape 7, respectively. Altan's updated script accordingly throws out the original script's layer code and uses a (not-quite-)cross-browser snowIE_NS6( ) function to reposition and move the snowflakes.

function snowIE_NS6( ) { ...     for (i = 0; i < no; ++i) { ...         document.getElementById("dot" + i) = yp[i] + "px";         document.getElementById("dot" + i).style.left = xp[i] + am[i] * Math.sin(dx[i]) + "px"; }     snowtimer = window.setTimeout("snowIE_NS6( );", 50); }

The Core DOM's document.getElementById( ) method, CSS 2's top and left properties...ah, now we're getting somewhere. The updated script repeats the original script's cumbersome snowflake div creation code, but we know how to deal with that.

The updated script has two new configuration options:
(1) A hidesnowtime variable can be set to a number that will stop the snowing and hide the snowflakes after number seconds.
(2) A snowdistance variable can be set to a pageheight string that causes the snowflakes to fall the entire height of the page (document content) for pages whose heights exceed the viewport height.
We will discuss these options and other differences between the updated and original scripts in this and the next posts.

More gatekeeperism

Not only does the updated script lose the layer code but it also uses an ns6up flag to lock out pre-6 versions of Netscape from some of its sections.

var ns6up = (document.getElementById && !document.all) ? 1 : 0;
/* Netscape and IE support for document.getElementById( ) began with Netscape 6 and IE 5, respectively. */

The updated script retains the original script's ie4up gatekeeper as it contains a small amount of code that was (not-quite-)IE only at the time the script was written.

var ie4up = (document.all) ? 1 : 0;

• The ie4up flag returns 1 for Opera 5-9.2; Opera 8 was the current version of Opera when the script was written.
Google Chrome wasn't around in 2005 but some early versions of Safari were; the latter were almost certainly green-lighted by the ns6up flag.
• As for modern browsers, the ie4up flag only returns 1 for IE whereas the ns6up flag returns 1 for the rest of the gang - Mozilla's browsers, Google Chrome, Safari, and Opera 9.5+ do support document.all but they "cloak" that support via returning false for document.all in a boolean context.

Of course, it would be preferable to not have to use the ie4up/ns6up flags in the first place, but it is not possible to discard them (and the proprietary code they guard) without leaving someone behind - more on this below.

Snow, snow, go away

As shown above, a setTimeout( )-mediated recursive call to the snowIE_NS6( ) function effects the script's snow animation.

snowtimer = window.setTimeout("snowIE_NS6( );", 50);

The snowIE_NS6( ) function is followed in the source by a hidesnow( ) function that clears the snowtimer timeout and hides each snowflake by setting its CSS visibility property to hidden.

function hidesnow( ) {
    if (snowtimer) window.clearTimeout(snowtimer);
    for (i = 0; i < no; i++) document.getElementById("dot" + i).style.visibility = "hidden"; }

If the value of the aforementioned hidesnowtime configuration variable is greater than 0, then a call to the hidesnow( ) function is scheduled just after the initial call to the snowIE_NS6( ) function.

// Configure whether snow should disappear after x seconds (0=never): var hidesnowtime = 7;
if (ie4up || ns6up) {
    snowIE_NS6( );
    if (hidesnowtime > 0) window.setTimeout("hidesnow( );", hidesnowtime * 1000); }

My minor cosmetic changes notwithstanding, the script's code for stopping and hiding the snow can be left alone, but we may need to get out some Mr. Clean for the next section...

Viewport dimensions, take 2

In demarcating the snowing field of action, the script first sets a snowdistance variable to a pageheight or windowheight string.

// Configure how much snow should drop down before fading ("windowheight" or "pageheight")
var snowdistance = "pageheight";

Next, the script reads the viewport width and height.

function iecompattest( ) {
    return (document.compatMode && document.compatMode != "BackCompat") ? document.documentElement : document.body; }

if (ns6up) {
    doc_width = self.innerWidth;
    doc_height = self.innerHeight; }
else if (ie4up) {
    doc_width = iecompattest( ).clientWidth;
    doc_height = iecompattest( ).clientHeight; }

For browsers that go through the ns6up gate, the window object's innerWidth and innerHeight are respectively assigned to doc_width and doc_height variables, as in the original script. (You may know that the innerWidth return includes the width of a vertical scrollbar if present and the innerHeight return includes the height of a horizontal scrollbar if present, but this is not an issue because these returns are adjusted to prevent any snowflakes from being repositioned outside the viewport.)

For browsers that go through the ie4up gate the situation is more complicated. The iecompattest( ).clientWidth and iecompattest( ).clientHeight expressions in the else if block call an iecompattest( ) function that tests (a) if the browser supports the compatMode property of the document object AND (b) if the document.compatMode return is not equal to the string BackCompat: if the browser is running in strict mode, for which document.compatMode returns CSS1Compat, then both tests pass and document.documentElement, a reference to the document's root/html element, is returned to the iecompattest( ) calls; if the browser is running in quirks mode, then document.body, a reference to the document's body element, is returned to the iecompattest( ) calls. Finally, document.documentElement.clientWidth or document.body.clientWidth is assigned to doc_width and document.documentElement.clientHeight or document.body.clientHeight is assigned to doc_height.

According to HowToCreate's "Window size and scrolling" tutorial, Microsoft has for IE 6+ designated document.documentElement.clientWidth/document.documentElement.clientHeight for returning the viewport dimensions in strict mode and document.body.clientWidth/document.body.clientHeight for returning the viewport dimensions in quirks mode. (As a Mac user I am unable to confirm this.) Recall that the corresponding else if block in the original script, which was written when IE 5 was the current version of IE, simply assigns document.body.clientWidth and document.body.clientHeight to doc_width and doc_height, respectively. IE 5 doesn't have a strict mode; indeed, according to Quirksmode's "Quirks mode and strict mode" tutorial (see The differences section) IE 5.5 effectively defines quirks mode for IE.

For the great majority of users,

doc_width = document.body.clientWidth;
doc_height = document.body.clientHeight;

will get the viewport dimensions when in quirks mode, and this is what I used to measure the viewport in my original (2012) demos for the original script. However, HowToCreate notes that with some browsers - Konqueror, some versions of Safari and iCab* - document.body.clientWidth and document.body.clientHeight return the page dimensions. HowToCreate would advise us to run as many browsers as possible through the innerWidth/innerHeight statements and then use clientWidth/clientHeight statements to accommodate IE 5-8 users, so maybe we should leave Altan's viewport-measuring code alone too.
(*Sure enough, my original script demos do not work with iCab 3.0.5 in the SheepShaver environment.)

HowToCreate claims that IE 9+ supports innerWidth/innerHeight only in strict mode, but neither Microsoft nor Dottoro says anything about this at their respective innerWidth/innerHeight pages.

When this post was first written the W3C wanted to store the viewport dimensions in document.documentElement.clientWidth/document.documentElement.clientHeight, and

doc_width = document.documentElement.clientWidth;
doc_height = document.documentElement.clientHeight;

does get the viewport width and height with all but one of the OS X GUI browsers on my computer when in strict mode - the exception is Opera 7.50, for which these statements get the page dimensions. (The statements return undefined with IE 5.2.3, but again, IE 5.x doesn't have a strict mode.)

Just looking at these expressions, what would you expect them to give you? Only self.innerWidth/self.innerHeight make sense as a means to obtain the viewport dimensions and they are accordingly supported by pretty much everyone these days. Intuitively both document.body.clientWidth/document.body.clientHeight and document.documentElement.clientWidth/document.documentElement.clientHeight should get the page dimensions, and sometimes they do but other times they don't. And what's with the client business? Why not just width and height? This isn't part of a Microsoft conspiracy to confuse us, is it?

Anyway, before moving on I should note that we discussed a very similar block of viewport-measuring code in the Viewport dimensions section of Blog Entry #240; this code was authored by Quirksmode and was deployed in the lightbox image viewer spotlighted by HTML Goodies' "How To Use the JavaScript Lightbox Image Viewer" tutorial. Quirksmode's code controls access to its innerWidth/innerHeight assignments via a self.innerHeight condition, which unlike the ns6up flag lets in Netscape 4.x, but nobody should be using Netscape 4.x anymore; it controls access to its document.documentElement assignments via a document.documentElement && document.documentElement.clientHeight condition, which converts to true for IE 6+ users in strict mode and to false for other IE users. Altan's use of a more general document.compatMode && document.compatMode != "BackCompat" condition for flagging users in strict mode allows him to functionize it so that he can call on it later.

The doc_width width is used to calculate a set of xp left offsets and the doc_height height is used to calculate a set of yp top offsets as in the original script. Before the snowflakes are repositioned, however, doc_width/doc_height are in the snowIE_NS6( ) function reset per the snowdistance variable; we'll check over these resets, and then roll out a demo, in our next episode.

Friday, November 16, 2012
Hearts of Snow
Blog Entry #270

We return now to our discussion of Altan d.o.o.'s original falling snow script. So we've got our snowflakes, which are holed up in the page's upper-left-hand corner. The script uses a snowNS( ) function or a snowIE( ) function to reposition and move the snowflakes. The snowNS( ) and snowIE( ) functions carry out the same types of operations; however:
(1) The snowNS( ) function was written (intended) for Netscape 4+ users; without making any changes it will only work with Netscape 4.x.
(2) The snowIE( ) function was written for IE 4+ users; without making any changes it will work with Chrome, Safari, and Opera as well as with IE 4+ upon removing the else if (document.all) { ... } gatekeeper that conditions its call in the top-level part of the script.

var ns4up = (document.layers) ? 1 : 0;
var ie4up = (document.all) ? 1 : 0;
if (ns4up) { snowNS( ); }
else if (ie4up) { snowIE( ); }
// End -->

The snowNS( ) and snowIE( ) functions employ a common set of six arrays to direct the snowing action:
(a-b) the snowflakes' vertical positionings are defined by yp and sty arrays;
(c-f) the snowflakes' horizontal positionings are defined by xp, am, dx, and stx arrays.
These arrays are declared and initialized in the top-level part of the script but no snowing occurs until we call the snowNS( ) or snowIE( ) function.

We will work with the snowIE( ) function, and the top-level code that supports it, in the sections below.

Vertical snow


The snowflakes are randomly distributed vertically by the following code:

var yp = new Array( );
var doc_height = document.body.clientHeight;
for (i = 0; i < no; ++i) { yp[i] = Math.random( ) * doc_height; }
function snowIE( ) {
    for (i = 0; i < no; ++i) { document.all["dot" + i].style.pixelTop = yp[i]; } }

As noted in Blog Entry #240, document.body.clientHeight reliably returns the height of the viewport as long as you're running in quirks mode, more specifically, as long as there isn't a document type declaration at the top of the document.

The random( ) method of the Math object returns a random number ranging from 0 (inclusive) to 1 (exclusive) and running umpteenbazillion places past the decimal point. The doc_height viewport height is multiplied by Math.random( ) no times to give a set of yp top offsets.

The snowIE( ) function sets each snowflake's style.pixelTop property to a yp[i] offset. As noted in the previous entry, pixelTop is not standard and is not supported by Mozilla's browsers; replacing it with top solves these problems. While we're at it, we should replace the document.all[ ] reference (whose argument should be delimited with parentheses and not square brackets) with a document.getElementById( ) reference. (The use of document.all throws a warning with Mozilla's browsers.)


If you look closely at my falling snow demo, you will notice that the snowflakes do not fall at the same rate, i.e., some snowflakes fall faster than others. The sty array

var sty = new Array( );
for (i = 0; i < no; ++i) { sty[i] = 0.7 + Math.random( ); }

determines the falling rate: every speed milliseconds, each snowflake drops 0.7-1.7 pixels per a sty[i] vertical step.

var speed = 20; /* Altan's chosen speed, 9, is too 'fast' for my taste. */
function snowIE( ) {
    for (i = 0; i < no; ++i) {
        yp[i] += sty[i];
        document.getElementById("dot" + i) = yp[i] + "px"; }
    window.setTimeout("snowIE( );", speed); }

Horizontal snow


A random set of xp left offsets for the snowflakes is produced as follows:

var xp = new Array( );
var doc_width = document.body.clientWidth; // Gives the viewport width when in quirks mode
for (i = 0; i < no; ++i) { xp[i] = Math.random( ) * (doc_width - 50); }

The -50 operation establishes a buffer that is evidently meant to prevent any snowflakes from going beyond the right edge of the viewport, which would enlarge the width of the page and generate a horizontal scrollbar if the page width does not exceed the viewport width. (In the Snowflake renewal section we'll see that the yp offsets also have such a buffer.)

The xp offsets are slightly increased or decreased via the sin( ) method of the Math object as detailed in the next subsection and then the modified offsets are respectively assigned to the snowflakes' style.pixelLefts.


If you go back and look at my demo, you'll see that each snowflake horizontally oscillates in a neighborhood surrounding its left offset as it falls; the size of the neighborhood varies from snowflake to snowflake and is determined by the am array.

var am = new Array( );
for (i = 0; i < no; ++i) { am[i] = Math.random( ) * 20; }

The am values serve as 'amplitudes' for Math.sin( ) operations that increment or decrement the xp offsets.

var dx = new Array( ), stx = new Array( );
for (i = 0; i < no; ++i) {
    dx[i] = 0;
    stx[i] = 0.02 + Math.random( ) / 10; }
function snowIE( ) {
    for (i = 0; i < no; ++i) {
        dx[i] += stx[i];
        document.all["dot" + i].style.pixelLeft = xp[i] + am[i] * Math.sin(dx[i]); } }

OK, trig fans, here we go. The Math.sin( ) method acts on an 'angle' measured in radians. The Math.sin( ) argument is not limited to 0-2π angles but can be any real-valued angle; accordingly, Math.sin( ) gives a periodic sine return - for the positive x-direction, the return begins at 0 and curves up to 1, it curves back down to 0 and then to -1, it curves back up to 0, etc. - check out the animation in the Relation to the unit circle section of Wikipedia's "Sine" entry. For our present purpose, we don't care about angles, radians, the unit circle, or any of that: we just want a function via which we can jiggle a snowflake about a common point, and Math.sin( ) will do that for us.

As shown, Math.sin( ) acts on the values of the dx array. Each dx[i] value is initialized to 0 and is incremented continually in the snowIE( ) function by a random 0.02-0.12 value from the stx array; as dx[i] increases, it passes through the π/2, 3π/2, 5π/2, ... values that give rise to the peaks and troughs of the Math.sin( ) output, thereby taking the snowflake to the right and left edges of its am neighborhood.

Suppose a snowflake's xp[i] value is 400 and its am[i] value is 10. When the snowflake is repositioned, its left offset begins at 400. As the snowflake falls, its left offset increases until it maxes out at 410, which occurs when dx[i] reaches π/2 and therefore Math.sin(dx[i]) returns 1; subsequently the left offset decreases until it reaches 390 (dx[i] reaches 3π/2, Math.sin(dx[i]) returns -1); the left offset then increases again until it reaches 410 (dx[i] reaches 5π/2, Math.sin(dx[i]) returns 1); and so on.
(OK, the left offset actually begins at a bit more than 400 because dx[i] is incremented to a non-0 value just before the repositioning. And the left offset does not exactly hit 410 and 390 because dx[i] does not exactly hit π/2, 3π/2, etc. But this is the gist of it.)

Other points:
(1) The sine and cosine functions have the same wave profile. Can we use the cos( ) method of the Math object in place of Math.sin( )? That we can.
(2) Per the preceding section, the left offset assignment should be recast as
document.getElementById("dot" + i).style.left = xp[i] + am[i] * Math.sin(dx[i]) + "px";
for a modern cross-browser script.

Snowflake renewal

Each snowflake is refreshed when it falls within a 50px buffer at the bottom of the page.

function snowIE( ) {
    for (i = 0; i < no; ++i) {
        if (yp[i] > doc_height - 50) {
            yp[i] = 0;
            xp[i] = Math.random( ) * (doc_width - am[i] - 30);
            sty[i] = 0.7 + Math.random( );
            stx[i] = 0.02 + Math.random( ) / 10;
            doc_height = document.body.clientHeight;
            doc_width = document.body.clientWidth; } } }

The snowflake is placed at the top of the viewport and is sent to a new random x-axis position. (Why a -am[i]-30 right buffer and not a -50 right buffer? Your guess is as good as mine.) The snowflake is given a new sty[i] vertical step and a new stx[i] feed for the Math.sin( ) operation.

The viewport height and width are also reread; these operations might at first glance seem unnecessary but they do have a purpose, namely, they effectively rescale the display if the user resizes the viewport.

The snowflake doesn't get a new am[i] neighborhood, so if you want the snowflake to be completely 'reborn', then you should add an am[i] = Math.random( ) * 20; statement to the if block. As a practical matter, however, subtracting the sty[i] and stx[i] resets and even the xp[i] reset has at most a minimal effect on the display.

Lastly and least, the snowflake's dx[i] value is not reset to 0: indeed, the dx[i] starting point is totally unimportant vis-à-vis the periodic Math.sin(dx[i]) output generated by dx[i]'s incrementation.

And that'll do it for our script deconstruction; there are some other changes I would make to the script - e.g., I would reorder some of the statements in a more logical way, I would merge the dx and stx arrays - but they're not worth belaboring. I'm not a snow engineer and I have no idea if the script accurately models the movement of snowflakes although the vertical and horizontal randomizations do make sense intuitively. You will still have a falling snow script if you unrandomize the sty/am/stx values, but the resulting display won't have quite the pizzazz of the original display.

Let me conclude this post with a second demo. For those of you for whom Lissa's demo does not work, here's what it looks like:

Love is in the air...

It's never too early to get ready for Valentine's Day, eh?

We'll discuss Altan's updated falling snow script in the following entry.

Monday, November 05, 2012
32 °F and Falling
Blog Entry #269

In this post we will begin discussing some of the JavaScript material at Lissa Explains It All, a Web site that provides basic information about creating Web pages and getting them on the Web; in particular we will check over the scripts offered by Section 1, Section 7, and Section 8 of the site's JavaScript subsector. The visual effects of these scripts are pretty cool: they include falling snow, a string that chases the mouse cursor, a bouncing image that leaves a trail, and even fireworks.

With one exception, the Sections 1/7/8 scripts are labeled "IE only", which is true in the sense that IE is the only modern browser that will run them. However, these scripts were not designed to be IE only. Written in the late 1990s, they all contain layer object code that enables them to be run by Netscape 4.x. (I've tested them with Communicator 4.61 in the SheepShaver environment, and they check out.) Our task is to update the scripts and to create cross-browser demos for them.

With no further ado, let's get going on the first Section 1 offering, a falling snow script authored by Altan d.o.o. A .zipped snowinstructions.txt file containing the script plus some commentary from Lissa can be downloaded here.

Lissa provides a hearts.html demo page that illustrates the script with a falling image, which can be downloaded as an abheart4.gif file at Lissa's Cursor Images page. Per the above discussion, the demo works with IE 4+ and Netscape 4.x but not with other browsers. (Actually, the demo might work with some older versions of Opera but it definitely does not work with newer versions of Opera.)

As for my own demo, here it is:

Let it snow! Let it snow! Let it snow! Winter is coming, and you may get some snow where you live although it's highly unlikely we'll see any here in the Crescent City.

BTW, an updated version of Altan's script is posted here (the snow3.gif image used by my demo comes from this page); we will look at this code later but for now we will work with the original code offered by Lissa's site.

Cross-browser obstacles

The falling snow script contains some cross-browser configuration code but is otherwise divided into separate Netscape and IE sections for creating, positioning, and moving the snowflakes; respective access to these sections is mediated by ns4up and ie4up gatekeepers.

var ns4up = (document.layers) ? 1 : 0;
var ie4up = (document.all) ? 1 : 0;

The ns4up parts of the code
(1) get the dimensions of the viewport by reading window.innerWidth and window.innerHeight,
(2) house the snowflakes in layer elements, and
(3) position/move the snowflakes via the layer object's top and left properties.

(1) is OK for most modern browsers (innerWidth/innerHeight are supported by IE 9+ but not by earlier IE versions) but (2) and (3) are only OK for Netscape 4.x.

Conversely, the ie4up parts of the code
(A) get the dimensions of the viewport by reading document.body.clientWidth and document.body.clientHeight,
(B) house the snowflakes in div elements, and
(C) position/move the snowflakes via document.all(divID).style.pixelTop and document.all(divID).style.pixelLeft expressions.

(A) is OK for modern browsers (Netscape's clientWidth/clientHeight support goes back to Netscape 7), as is (B). As for (C), modern browsers support document.all in a non-boolean context but the proprietary CSS pixelTop and pixelLeft properties are not supported by Mozilla's browsers; however, it is simple enough to respectively replace pixelTop and pixelLeft with their standard top and left counterparts*.

*pixelTop/pixelLeft and top/left are not exactly equivalent in that the former's values have a number type whereas the latter's values have a string type; we will want to append px unit strings to the latter's values.

We can profitably use the ie4up code as our base for a modern cross-browser script; the ns4up code can and should be thrown out. The ie4up code parts are wrapped in if (ie4up) { ... } statements that lock out modern non-IE browsers, for which the ie4up/document.all condition returns false (we previously discussed the somewhat schizophrenic approach of these browsers to document.all support in Blog Entry #244). To allow non-IE access to the ie4up code, you can replace the document.all test with a document.getElementById test or you can just throw out the if (ie4up) { ... } wrappers altogether.

Snowflake creation

A no variable sets the number of snowflakes in the display; a snowflake variable holds the URL of the snowflake image.

var no = 15;
var snowflake = "snow3.gif";

A no-iteration for loop creates and writes to the page no div elements that individually contain an img element whose src is set to snowflake.

for (i = 0; i < no; ++i) {     ...     if (ie4up) {         if (i == 0) {             document.write("<div id='dot"+ i +"' style='position: ");             document.write("absolute; z-index: "+ i +"; visibility: ");             document.write("visible; top: 15px; left: 15px;'><img src='");             document.write(snowflake + "' border='0'></div>");         } else {             document.write("<div id='dot"+ i +"' style='position: ");             document.write("absolute; z-index: "+ i +"; visibility: ");             document.write("visible; top: 15px; left: 15px;'><img src='");             document.write(snowflake + "' border='0'></div>"); } } }

The divs are respectively given ordinalized ids: dot0, dot1, dot2, etc.

The divs are collectively given position, z-index, visibility, top, and left style settings; the position:absolute; style is necessary but the others aren't:
(a) The divs are added to the document body in source order and are thus z-stacked in source order (à la the pole/fish images of the DHiNC Swimming Fish Example), so we don't need to give them z-indexes.
(b) The initial value of the CSS visibility property is already visible.
(c-d) The top:15px;left:15px; declaration set unnecessarily specifies an initial position for the divs before they are distributed across the viewport; letting top and left default to auto places the divs at their normal-flow position(s), which is good enough for our purposes.

None of the browsers on my computer imparts a default border to an image; you can keep the border='0' attribute (or, given its deprecation, replace it with a corresponding style rule) if you want but I would get rid of it.

Is there any reason to deal with the dot0 div via the if (i == 0) { ... } statement and then use a separate else clause for the remaining divs? None whatsoever. In his updated code (vide supra), Altan does wrap the dot0 div's image in an <a href=''> ... <\/a> anchor, but I fail to see the point of this.

For that matter, do we need the div containers in the first place? Nope - there's no reason not to directly manipulate the images themselves. Putting it all together, the snowflake creation code can be simplified to:

img { position: absolute; }
for (i = 0; i < no; ++i) { document.write("<img id='dot" + i + "' src='" + snowflake + "'>"); }

We're ready to strew our snowflakes across the page and get the snowing action under way - we'll go through the script's snow mechanics in detail in the following entry.

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)