reptile7's JavaScript blog
Friday, January 25, 2013
The Sidewinder String
Blog Entry #277

In today's post we will work through Peter Gehrig's "cursor trailer" script, the last script offered by Section 1 of the JavaScript subsector of Lissa Explains It All. The cursor trailer script tethers a text string to the mouse cursor and then causes the string to follow the cursor when the cursor is moved around the viewport area - as such it is reminiscent of the "cat chases your mouse" script discussed by HTML Goodies' JavaScript Script Tip #91, which we covered in Blog Entries #107 and #108.

A .zipped trailinstructions.txt file containing the script plus a bit of commentary can be downloaded here and a textcursor.html demo for the script is posted here. The script and demo are currently IE only (they'll also work with Netscape 4.x and Opera 5-9.2), but we'll have them spiffed up for other modern browsers by the end of the post.

Before we get started, I should note that Peter has posted an updated, somewhat more involved version of the cursor trailer script at, if you'd like to check that out.


For the textcursor.html demo Lissa works with a Lissa Explains it All! trailer string:

// Your snappy message. Important: the space at the end of the sentence!!!
var message = "Lissa Explains it All! ";

Actually, we'll see later that the space at the end of the message string doesn't need to be there at all. The message string is subsequently atomized by the split( ) method of the String object:

message = message.split("");

The split( ) operation gives a message array whose boxes respectively contain the characters of the original message string: message[0] is L, message[1] is i, message[2] is s, and so on. (A string is ultimately an array of characters.) The message elements are wrapped in span elements

for (i = 0; i <= message.length - 1; i++) {
    document.write("<span id='span" + i + "' class='spanstyle'>");
    document.write("</span>"); }

so that we can apply CSS to them. The spans are respectively given ordinalized ids: span0, span1, span2, etc. The following styles are initially applied to the spans:

.spanstyle {
    visibility: visible; /* Unnecessary - visible is the initial visibility value. */
    position: absolute; top: -50px;
    color: black; font-family: comic sans ms; font-weight: bold; font-size: 10pt; }

When the page loads, and before we make any mousemoves in the viewport area, a recursive makesnake( ) function is set in motion; however, the makesnake( ) function doesn't do anything until a boolean flag variable is switched from 0 to 1.

var flag = 0;

function makesnake( ) {
    timer = window.setTimeout("makesnake( );", 30); }

<body onload="makesnake( );">

Mousemove monitoring

A handlerMM( ) function that listens for mousemove events is registered on the document object:

if (document.layers) { document.captureEvents(Event.MOUSEMOVE); }
document.onmousemove = handlerMM;

When we make a mousemove, the handlerMM( ) function gets the mousemove's final coordinates and also turns on the flag flag.

var x, y;

function handlerMM(e) {
    x = (document.layers) ? e.pageX : document.body.scrollLeft + event.clientX;
    y = (document.layers) ? e.pageY : document.body.scrollTop + event.clientY;
    flag = 1; }

The document.layers condition notwithstanding, the handlerMM( ) function as originally written will work with Google Chrome and Safari and Opera as well as with IE; Mozilla's browsers, which do not support Microsoft's window.event object, are left out in the cold. However, Mozilla's browsers do support the clientX/clientY event object properties and the scrollLeft/scrollTop element object properties and are easily brought into the loop by recasting handlerMM( ) as:

function handlerMM(e) {
    e = e ? e : event;
    x = document.body.scrollLeft + e.clientX;
    y = document.body.scrollTop + e.clientY;
    flag = 1; }

FYI: Netscape's clientX/clientY support began with Netscape 6 and its scrollLeft/scrollTop support began with Netscape 7 and therefore the preceding handlerMM( ) won't work with Netscape 6, but there's no reason on earth why anybody should be using Netscape 6 in this day and age. But if you want to include an

if (document.getElementById && !document.all && !document.body.clientWidth) { x = e.pageX; y = e.pageY; }

statement for any Netscape 6 users out there, I won't stop you. ;-)

Trail it

When the handlerMM( ) function has finished executing, the aforementioned makesnake( ) function springs into action. The makesnake( ) function separates successive message characters by a step length and vertically aligns the characters with the mouse cursor, i.e., the characters have the cursor's y coordinate. The makesnake( ) function body comprises an if...else if statement whose if clause caters to IE and whose else if clause caters to Netscape; we will work with the if/IE clause in the discussion that follows.

var step = 20;
var xpos = new Array( );
for (i = 0; i <= message.length - 1; i++) { xpos[i] = -50; }
var ypos = new Array( );
for (i = 0; i <= message.length - 1; i++) { ypos[i] = -50; }

function makesnake( ) {
    if (flag == 1 && document.all) {
        for (i = message.length - 1; i >= 1; i--) {
            xpos[i] = xpos[i - 1] + step;
            ypos[i] = ypos[i - 1]; }
        xpos[0] = x + step;
        ypos[0] = y;
        for (i = 0; i < message.length - 1; i++) {
            var thisspan = eval("span" + (i) + ".style");
            thisspan.posLeft = xpos[i];
            thisspan.posTop = ypos[i]; } }
    /* Netscape part */
    var timer = window.setTimeout("makesnake( );", 30) }

Left and top offsets for the message characters are respectively stored in xpos and ypos arrays, which are declared and initialized in the top-level part of the code.

The if clause's first for loop calculates, but does not set, left and top offsets for all of the message characters except the zeroth character (message[0]=L for Lissa's string), whose left and top offsets are calculated by the subsequent two statements; a second for loop actually sets the offsets. Changing the second loop's condition to
i <= message.length - 1 or i < message.length
obviates the need to put a space at the end of the original message string.

In setting the offsets, Peter references the message character span wrapper objects with their "span"+i ids - a practice that Microsoft itself discommends - and makes use of the posLeft and posTop pair of properties, which, like the pixelLeft/pixelTop property pair discussed in Blog Entry #269, is basically a unitless version of the standard CSS left/top property pair and is not supported by Mozilla's browsers. The makesnake( ) function will work with Google Chrome, Safari, and Opera if we shrink its if gate to if(flag); if we also rewrite its second loop as

for (i = 0; i < message.length; i++) {
    var thisspan = document.getElementById("span" + i); = xpos[i] + "px"; = ypos[i] + "px"; }

then it'll work with Mozilla's browsers as well.

Note that the offset-calculating code runs in reverse order - for Lissa's 23-character string, the first loop calculates offsets for message[22] in the first iteration, for message[21] in the second iteration, for message[20] in the third iteration, etc. - even as the offset-setting code runs in forward order; as a result, message.length makesnake( ) runs are required for the message string to reach its final, cursor-tethered form. Suppose our first mousemove puts the cursor at (x,y) = (100,100). The first flag==1 run puts the L of Lissa's string at (120,100) and the following characters at (-30,-50); the next run puts the i at (140,100) and the following characters at (-10,-50); the next run puts the s at (160,100) and the following characters at (10,-50); and so on.

Managing the offsets in this way imparts a fluid, snakelike motion to the message string when the cursor is moved around the viewport area. Running both the offset-calculating code and the offset-setting code in forward order, for which only one loop is necessary -

for (i = 0; i < message.length; i++) {
    xpos[i] = i ? (xpos[i - 1] + step) : (x + step);
    ypos[i] = i ? ypos[i - 1] : y;
    var thisspan = document.getElementById("span" + i); = xpos[i] + "px"; = ypos[i] + "px"; }

- will still give you a cursor-trailing string, but in this case the resulting message motion is much more rigid (although not devoid of fluidity), as only one makesnake( ) run is required for the message string to reach its final form.

The step variable effectively sets a uniform distance between the left edges of each message[i-1] and message[i] pair of characters and between the cursor and the left edge of the message[0] character. À la normal text, the step setting does not produce a uniform intercharacter spacing unless you're working with a monospace font (e.g., Courier, which I use in my demo below).


In the trailinstructions.txt file, Lissa puts </textarea></form> right after Peter's script and twice instructs the reader to add

<form action=URI>
<textarea name="S1" cols="40" wrap="virtual" style="width:90%;background:white;font-family:verdana;color:black;border-style:solid;">

to the document body. The form/textarea markup serves no purpose and should be thrown out; indeed, it doesn't appear in the textcursor.html source.

The textcursor.html document contains two body elements:

<body bgcolor="#00ff00">
<body onload="makesnake( );" style="width:100%;overflow-x:hidden;overflow-y:scroll;">

Only one body element end-tag appears at the end of the document. Given that the body element's end-tag is optional,
(1) the body elements can be viewed as siblings, in which case we're violating the content model of the html element, or
(2) the second body element can be viewed as a child of the first body element, in which case we're violating the content model of the body element (although the body element is a block-level element, it is not part of the %block; family of elements).
For the record, the html element is the only element that can contain the body element, and it can only contain one body element.

You can put the bgcolor, onload, and style attributes in a single body element start-tag if you want; my preference is to move the background color setting to a style sheet, trigger the makesnake( ) function with a window.onload = makesnake; JavaScript statement, and throw out the width/overflow-x/overflow-y stylings*.

*Assuming that some content is present, the body element already has a default width of 100% (i.e., its width spans the width of the viewport). The overflow-x and overflow-y properties are described here: the overflow-x:hidden; declaration prevents the generation of a horizontal scrollbar if the message string crosses the right edge of the viewport whereas the overflow-y:scroll; declaration ensures the generation of a vertical scrollbar if the message string crosses the bottom edge of the viewport. Are these effects crucial? They're not crucial.


Mouseover the aliceblue region below; wildly move the cursor around and then stop the movement.

Run your cursor over this div =)

Carnival is under way - throw me something, Mister!

Moving to Section 7 of the JavaScript subsector, we'll take on Peter's "floating images" script in the following entry.

Comments: Post a Comment

<< Home

Powered by Blogger

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