reptile7's JavaScript blog
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.

Comments: Post a Comment

<< Home

Powered by Blogger

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