Chapter 15

Visual Effects


CONTENTS

The flat, static Web page may not yet be a thing of the past. But as the number of pages on the Web spirals into the tens of millions, new creative approaches are required both to catch viewers' attention and to hold their interest. Web page designers may now choose from a growing array of tools to lend visual impact to their creations.

In this chapter, you'll see how JavaScript can be used to create several useful visual effects, including alternating color schemes, fades, scrolling marquees, and dynamic graphics. Unlike effects created using other tools, JavaScript effects load quickly as part of your document, and can start even before a page is completely loaded. n

Creating Dynamic Framesets

Before we get started, let's take a look at the frameset environment we'll use to create visual effects.

Because Netscape 2.0 does not provide a way to update a document directly once it has been written to the screen, all of the effects we create here require that we write a new document to the screen for each step in an animation, marquee, or other effect. Rather than load each successive document from the server (which would be much too slow), we'll generate our documents on-the-fly and then slap them into frames. Listing 15.1 shows the skeleton frameset that we'll develop in the examples that follow.


Listing 15.1  Skeleton Frameset

<html>
<head>
<title>Visual Effects</title>
<script language="JavaScript">
<!- begin script
var emptyFrame = '<html></html>';
function headFrame () {
  return '<html><body bgcolor="#FFFFFF" text="#000000">' +
    '<h1 align="center">Visual Effects</h1>' +
    '</body></html>';
}
function initialize () {
  self.head.location = "javascript:parent.headFrame()";
}
// end script ->
</script>
<frameset rows="52,*" onLoad="initialize()">
  <frame name="head" src="javascript:parent.emptyFrame"
     marginwidth=1 marginheight=1 scrolling="no" noresize>
  <frame name="body" src="javascript:parent.emptyFrame">
</frameset>
<noframes>
<h2 align="center">Netscape 2.0 or other
JavaScript-enabled browser required</h2>
</noframes>
</html>

Some of the syntax and techniques used here are a bit different from what you've seen so far. But it's all perfectly legal. Let's take a minute to dissect this skeleton script.

The javascript: Protocol

You've often seen http: or ftp: at the beginning of a URL. This is the protocol: it tells the browser which protocol handler to use to retrieve the object referred to after the colon. The javascript: protocol is really no different; it simply instructs the browser to let JavaScript retrieve the object. But rather than initiate communication with another server, the JavaScript handler returns the value of the variable or function cited after the colon. The value returned should be HTML or some other MIME type the browser knows how to display.

When using a javascript: URL, keep in mind that the reference after the colon is specified from the perspective of the receiving frame. In our example, from the point of view of the head frame, the headFrame() function is in the parent frame.

Empty Frames

Sometimes it's desirable to initially leave a frame blank and load the contents later. For instance, the value of the frame may depend on user input or the result of a lengthy calculation or process. You could load an empty document from the server, but that wastes a server access. It's faster and easier to use the javascript: protocol to load the empty document internally.

The <HTML></HTML> pair used in our emptyFrame variable isn't strictly necessary under Netscape-an empty string works just as well. But other JavaScript-enabled browsers, when they're available, may not be as forgiving.

You may be wondering why we need to use an empty frame at all in this example, at least for the head frame, when we could load it directly. In fact, it should not be necessary to do this, but a bug in Netscape 2.0 causes frames loaded using the javascript: protocol to align incorrectly if they are loaded directly from a FRAME tag. So instead, we must load an empty document in the FRAME tag and then load the intended document from the onLoad handler for the frameset.

You might also be tempted to simply leave off the SRC= attribute. However, due to another odd Netscape behavior, frames that do not have an initial location specified cannot be updated with a new location.

NOTE
You may have seen frameset documents that use about:blank to specify an empty frame. This is a Netscape-specific construction and should be avoided. Also, frames initialized with about:blank have been known to display spurious messages on some platforms.

Content Variables versus Functions

In our skeleton frameset, emptyFrame is a variable containing HTML, while headFrame() is a function that returns HTML. Either method can be used. In general, use variables if the content will not change. Use functions to return dynamic content.

A Simple Color Alternator

One of the easiest visual effects to create is a color alternator, which switches between two color schemes in a frame. This effect is best used in small frames containing large, bold headlines. It should not generally be used with smaller, detail text, as it will make such text difficult to read while the effect is in progress. It would also be wise to use this effect in moderation-a brief burst of alternating colors can be very effective when your page first loads, when making a transition to a new topic, or to underscore a point. However, continuous flashing quickly becomes annoying to the viewer. (Remember the fate of the BLINK tag!)

Let's start with a simple, direct example. Building on our skeleton frameset, in listing 15.2, we modify the headFrame() function to return one of two BODY tags, depending on the state of a variable called headColor.


Listing 15.2  The Modified headFrame() Function

var headColor = "white";
function headFrame () {
  return '<html>' +
    ((headColor == "white") ?
      '<body bgcolor="#FFFFFF" text="#000000">' :
      '<body bgcolor="#000000" text="#FFFFFF">') +
    '<h1 align="center">Visual Effects</h1>' +
    '</body></html>';
}

In listing 15.3, we create a function called setHead() that uses JavaScript's setTimeout() function to create a timer loop. We'll update the head frame six times, alternating colors each time.


Listing 15.3  The setHead() Function Alternates Colors

var headLoops = 6;
function setHead () {
  if (headColor == "white")
    headColor = "black";
  else
    headColor = "white";
  if (-headLoops > 0)
    setTimeout ('setHead()', 100);
  self.head.location = "javascript:parent.headFrame()";
}

Finally, we'll call setHead() in our initialize() function:

function initialize() {
  setHead();
}

When our example page is loaded, the head frame will alternate rapidly between white-on-black and black-on-white. The entire effect lasts less than one second. The output is shown in figures 15.1 and 15.2.

Figure 15.1 : The heading frame alternates between black-on-white (shown here) and and white-on-black (shown in fig. 15.2).

Figure 15.2 : Here is an example of white-on-black.

NOTE
Due to an implementation problem in Netscape 2.0, timer events are called a maximum of three times per second on Windows platforms. This limitation is expected to be removed in a future release.

Listing 15.4 shows the complete code for this example.


Listing 15.4  The Simple Color Alternator

<html>
<head>
<title>Visual Effects</title>
<script language="JavaScript">
<!-- begin script
var emptyFrame = '<html></html>';
var headColor = "white";
function headFrame () {
  return '<html>' +
    ((headColor == "white") ?
      '<body bgcolor="#FFFFFF" text="#000000">' :
      '<body bgcolor="#000000" text="#FFFFFF">') +
    '<h1 align="center">Visual Effects</h1>' +
    '</body></html>';
}
var headLoops = 6;
function setHead () {
  if (headColor == "white")
    headColor = "black";
  else
    headColor = "white";
  if (-headLoops > 0)
    setTimeout ('setHead()', 125);
  self.head.location = "javascript:parent.headFrame()";
}
function initialize () {
  setHead();
}
// end script -->
</script>
<frameset rows="52,*" onLoad="initialize()">
  <frame name="head" src="javascript:parent.emptyFrame"
     marginwidth=1 marginheight=1 scrolling="no" noresize>
  <frame name="body" src="javascript:parent.emptyFrame">
</frameset>
<noframes>
<h2 align="center">Netscape 2.0 or other
JavaScript-enabled browser required</h2>
</noframes>
</html>

A Better Color Alternator

Our first color alternator works fine if you plan to only use the effect once with one set of colors in a single frame. But if you plan to use this effect more extensively, you'll end up duplicating a lot of code. In this section, we'll develop a generalized version of the color alternator that offers greater flexibility and can easily be reused.

We'll take an object-oriented, component-based approach in this example. This might initially appear to be overkill, but as you will see, the components we create here provide the foundation for more complex effects.

Color Objects

Let's start by defining a Color object and some related functions. As you know, colors in HTML (and JavaScript) are represented by hexadecimal triplets of the form RRGGBB, in which each two-digit hexadecimal code represents the red, green, or blue component of a color. Values range between 00 and FF hex, corresponding to zero to 255 decimal. Our Color object constructor, shown in listing 15.5, accepts a hexadecimal string, but stores the individual components as numbers, which are easier to manipulate.


Listing 15.5  The Color Object Constructor

var hexchars = '0123456789ABCDEF';
function fromHex (str) {
  var high = str.charAt(0); // Note: Netscape 2.0 bug workaround
  var low = str.charAt(1);
  return (16 * hexchars.indexOf(high)) +
    hexchars.indexOf(low);
}
function toHex (num) {
  return hexchars.charAt(num >> 4) + hexchars.charAt(num & 0xF);
}
function Color (str) {
  this.red = fromHex(str.substring(0,2));
  this.green = fromHex(str.substring(2,4));
  this.blue = fromHex(str.substring(4,6));
  this.toString = ColorString;
  return this;
}
function ColorString () {
  return toHex(this.red) + toHex(this.green) + toHex(this.blue);
}

As you might expect, the fromHex() and toHex() functions convert between numeric and hexadecimal values. Note that these functions will only work with values in the range 00 to FF hex (zero to 255 decimal). By the way, it should be possible to write the fromHex() function more compactly, as

function fromHex (str) {
return (16 * hexchars.indexOf(str.charAt(0))) +
    hexchars.indexOf(str.charAt(1));
}

However, a bug in the JavaScript implementation in Netscape 2.0 prevents this from working correctly.

The ColorString() function is defined as the Color object's toString() method. This function converts the color back to an RGB triplet, and is automatically invoked any time a Color object is used in a context that requires a string.

TIP
Any JavaScript object can be given a toString() method, which is automatically called whenever an object needs to be converted to a string value.

Let's use the Color constructor to define a few colors:

var black = new Color ("000000");
var white = new Color ("FFFFFF");
var blue = new Color ("0000FF");
var magenta = new Color ("FF00FF");
var yellow = new Color ("FFFF00");

Now that we've got our colors in a convenient form, let's define an object to contain all the colors in use by a document at a given moment. We'll call this the BodyColor object. Its constructor is shown in listing 15.6.


Listing 15.6  The BodyColor Object Constructor

function BodyColor (bgColor,fgColor,linkColor,vlinkColor,alinkColor) {
  this.bgColor = bgColor;
  this.fgColor = fgColor;
  this.linkColor = linkColor;
  this.vlinkColor = vlinkColor;
  this.alinkColor = alinkColor;
  this.toString = BodyColorString;
  return this;
}
function BodyColorString () {
  return '<body' +
    ((this.bgColor == null) ? '' : ' bgcolor="#' + this.bgColor + '"') +
    ((this.fgColor == null) ? '' : ' text="#' + this.fgColor + '"') +
    ((this.linkColor == null) ? '' : ' link="#' + this.linkColor + '"') 
          +
    ((this.vlinkColor == null) ? '' : ' vlink="#' + this.vlinkColor +   
          '"') +
    ((this.alinkColor == null) ? '' : ' alink="#' + this.alinkColor + 
          '"') +
    '>';
}

The BodyColor() constructor accepts up to five Color objects as parameters, one for each HTML body color attribute. The colors are specified in the order of generally accepted importance; trailing colors can be omitted if they will not be used. So, for instance, if a document does not contain any links, the last three parameters can safely be left off.

Like the Color constructor, the BodyColor constructor includes a toString() method: the BodyColorString() function. In this case, a complete BODY tag is returned, including any color attributes specified.

Note that the individual Color objects are used directly in the construction of the BODY tag string. Because they are used in a context requiring a string, the Color object's toString() method will automatically be called to translate these into hexadecimal triplet strings!

Let's define a few BodyColor objects. Because we won't be using any links in this example, we'll omit the three link parameters:

var blackOnWhite = new BodyColor (white, black);
var whiteOnBlack = new BodyColor (black, white);
var blueOnWhite = new BodyColor (white, blue);
var magentaOnYellow = new BodyColor (yellow, magenta);
var yellowOnBlue = new BodyColor (blue, yellow);

In this case, we've used colors we defined previously. Because we're likely to reuse these colors, it was worthwhile to assign them to named variables. But suppose we wanted to use a color only once in a specific BodyColor object. It seems-and is-inefficient to define a variable just to hold an object we're going to use immediately:

var weirdOne = new Color ("123ABC");
var oddBody = new BodyColor (weirdOne, yellow);

Instead, we can invoke the Color constructor directly from the BodyColor constructor parameter list without ever assigning a name to the color:

var oddBody = new BodyColor (new Color ("123ABC"), yellow);

TIP
When creating an object that is referred to by name only once, you can invoke its constructor in the parameter list of the function or method that will use it instead of assigning it to a named variable

The Alternator Object

Our next step is to create an object that generates HTML that alternates between two BodyColor specifications. We'll call this the Alternator object. Its constructor is shown in listing 15.7.


Listing 15.7  The Alternator Object Constructor

function Alternator (bodyA, bodyB, text) {
  this.bodyA = bodyA;
  this.bodyB = bodyB;
  this.currentBody = "A";
  this.text = text;
  this.toString = AlternatorString;
  return this;
}
function AlternatorString () {
  var str = "<html>";
  with (this) {
    if (currentBody == "A") {
      str += bodyA;
      currentBody = "B";
    }
    else {
      str += bodyB;
      currentBody = "A";
    }
    str += text + '</body></html>';
  }
  return str;
}

The Alternator() constructor accepts two BodyColor objects plus a string containing whatever is to appear between <BODY> and </BODY>. In theory, the text string can be arbitrarily long, but 4K seems to be the maximum usable length on some Netscape platforms. In our examples, this string will be much shorter.

The currentBody variable indicates which BodyColor object is used to generate the BODY tag. This is switched by the toString() method, AlternatorString(), each time it is invoked.

Let's create an Alternator object now. We'll use the same text that appeared in the head frame of our simple alternator example:

var flashyText = new Alternator (blackOnWhite, whiteOnBlack,
  '<h1 align="center">Visual Effects</h1>');

Each time flashyText is referenced, it will alternate between black-on-white and white-on-black output. For example, suppose we loaded flashyText into three frames consecutively:

self.frameA.location = "javascript:parent.flashyText";
self.frameB.location = "javascript:parent.flashyText";
self.frameC.location = "javascript:parent.flashyText";

The output is shown in figure 15.3.

Figure 15.3 : The Alternator object alternates between color schemes each time it is used.

Events and the Event Queue

All that's left is to write our flashyText object to the screen at regular intervals. To do this, we'll create an object called an Event, which-in this context-is an action that is scheduled to take place at a particular time. We can define our Event object so that a separate event was required for each write to the screen, but this would require a lot of extra coding. Instead, we'll build a looping mechanism into our Event object because most of the effects we create in this chapter involve multiple writes to the screen. Listing 15.8 shows the Event constructor.


Listing 15.8  The Event Object Constructor

function Event (start, loops, delay, action) {
  this.start = start * 1000;
  this.next = this.start;
  this.loops = loops;
  this.loopsRemaining = loops;
  this.delay = delay * 1000;
  this.action = action;
  return this;
}

The Event constructor takes the start time for the event (relative to the time the program is launched or the time the EventQueue is started), the number of times (loops) to execute the event, the delay between each execution, and the action to be performed for the event.

The start time and delay are specified in seconds, but are converted to milli-seconds for internal use. The action can be any valid JavaScript statement en-closed in quotes. (This is similar to the way you specify an action for JavaScript's setTimeout() function.) The following is the Event constructor for our flashyText object:

var flashEvent = new Event (0, 10, 0.1,
  'self.head.location="javascript:parent.flashyText"');

We will start the event at time zero, that is, as soon as the EventQueue is started. We'll loop ten times with each loop one-tenth of a second apart. The action for the event is to load the flashyText object into the head frame.

We've defined an Event, but it's still just sitting there. This is where the EventQueue object comes in. The EventQueue contains a list of Event objects to be acted upon. It handles the scheduling and looping of events and executes the associated actions. Listing 15.9 shows the EventQueue constructor and related functions. This is a fairly complex bit of code; I won't go through it line-by-line, but, following the listing, I'll cover the key parameters and methods.


Listing 15.9  The EventQueue Object Constructor

function EventQueue (name, delay, loopAfter, loops, stopAfter) {
  this.active = true;
  this.name = name;
  this.delay = delay * 1000;
  this.loopAfter = loopAfter * 1000;
  this.loops = loops;
  this.loopsRemaining = loops;
  this.stopAfter = stopAfter * 1000;
  this.event = new Object;
  this.start = new Date ();
  this.loopStart = new Date();
  this.eventID = 0;
  this.addEvent = AddEvent;
  this.processEvents = ProcessEvents;
  this.startQueue = StartQueue;
  this.stopQueue = StopQueue;
  return this;
}
function AddEvent (event) {
  this.event[this.eventID++] = event;
}
function StartQueue () {
  with (this) {
    active = true;
    start = new Date();
    loopStart = new Date();
    loopsRemaining = loops;
    setTimeout (name + ".processEvents()", this.delay);
  }
}
function StopQueue () {
  this.active = false;
}
function ProcessEvents () {
  with (this) {
    if (!active) return;
    var now = new Date();
    if (now.getTime() - start.getTime() >= stopAfter) {
      active = false;
      return;
    }
    var elapsed = now.getTime() - loopStart.getTime();
    if (elapsed >= loopAfter) {
      if (-loopsRemaining <= 0) {
        active = false;
        return;
      }
      loopStart = new Date();
      elapsed = now.getTime() - loopStart.getTime();
      for (var i in event)
        if (event[i] != null) {
          event[i].next = event[i].start;
          event[i].loopsRemaining = event[i].loops;
        }
    }
    for (var i in event)
      if (event[i] != null) // Note: Netscape 2.0 bug workaround
        if (event[i].next <= elapsed)
          if (event[i].loopsRemaining- > 0) {
            event[i].next = elapsed + event[i].delay;
            eval (event[i].action);
          }
    setTimeout (this.name + ".processEvents()", this.delay);
  }
}

The first parameter to the EventQueue constructor is the queue name. This must be the same as the variable name to which the EventQueue object is assigned. (This is a bit of a kluge, but is required for the event processor to make setTimeout() calls to itself.)

Next, the delay parameter specifies how often the events in the queue are checked. This is important because it determines the maximum rate of actions for all events in the queue. If you specify a queue delay of 0.10 seconds, but an event delay of 0.05 seconds, the event will only be executed every 0.10 seconds. Therefore, the delay should be set to the smallest value required by your events. Values smaller than 0.05 seconds are not recommended.

CAUTION
Due to a bug in Netscape 2.0, memory allocated to the action string in a setTimeout() call is not released until the page is exited. Therefore, because each processing loop of the EventQueue object calls setTimeout(), set the delay to the highest usable value to minimize calls to setTimeout(). This bug is expected to be fixed in a future release.

The loopAfter parameter specifies the number of seconds after which the entire EventQueue will start over. This enables entire complex sequences of events to be repeated.

The loops parameter specifies the number of times the entire EventQueue repeats. Set this to zero if you do not want the queue to repeat.

The stopAfter parameter indicates the number of seconds after which the queue stops processing events, regardless of the number of loops remaining. Set this to an arbitrarily chosen high number, such as 99999, if you do not want the queue to stop after any particular length of time.

Once the EventQueue has been defined, you can then use the addEvent() method to add events to the queue. Let's create an event queue and add our flashEvent object to it.

var evq = new EventQueue ("evq", 0.1, 30, 10, 99999);
evq.addEvent (flashEvent);

Our event queue will check for events every 0.1 seconds. It will start over every 30 seconds, repeating 10 times. If for some reason it is still active after 99999 seconds, it will stop processing.

The final step is to start the queue. We'll do this in our initialize() function, which is the onLoad handler for our frameset.

function initialize () {
  evq.startQueue();
}

That's it! We're in business! After creating numerous functions and scores of lines of code, we now have exactly what we started with in our first, "simple" example. But wait-there's more.

Scheduling Multiple Events

As noted at the beginning of this section, this somewhat complex approach to generating the Alternator effect isn't really necessary if you are only going to use a single effect once in your program. But the advantages quickly multiply when you create complex effects or sequences of events. Each new event requires just a few lines of code, as listing 15.10 demonstrates.


Listing 15.10  Adding New Alternator Events

var dance1 = new Alternator (yellowOnBlue, magentaOnYellow,
  '<h1 align="center">Dancing...</h1>');
var inthe1 = new Alternator (magentaOnYellow, yellowOnBlue,
  '<h1 align="center">...in the...</h1>');
var streets1 = new Alternator (whiteOnBlack, yellowOnBlue,
  '<h1 align="center">...streets!</h1>');
var d1e = new Event (0, 10, .1,
  'self.f1.location="javascript:parent.dance1"');
var i1e = new Event (3, 10, .1,
  'self.f1.location="javascript:parent.inthe1"');
var s1e = new Event (6, 10, .1,
  'self.f1.location="javascript:parent.streets1"');
evq.addEvent(d1e);
evq.addEvent(i1e);
evq.addEvent(s1e);

Listing 15.11 shows the complete code for the improved alternator with an
expanded example. (This script can also be found in the file 15exm01.htm on the CD-ROM.) The output is shown in figure 15.4. In the sections that follow, you'll see how you can easily build on our event model to create even more interesting effects.

Figure 15.4 : Alternating text events are scheduled in four frames.


Listing 15.11  15exm01.htm  Complete Code for the Improved Alternator

<html>
<head>
<title>Visual Effects</title>
<script language="JavaScript">
<!- begin script
var emptyFrame = '<html></html>';
var hexchars = '0123456789ABCDEF';
function fromHex (str) {
  var high = str.charAt(0); // Note: Netscape 2.0 bug workaround
  var low = str.charAt(1);
  return (16 * hexchars.indexOf(high)) +
    hexchars.indexOf(low);
}
function toHex (num) {
  return hexchars.charAt(num >> 4) + hexchars.charAt(num & 0xF);
}
function Color (str) {
  this.red = fromHex(str.substring(0,2));
  this.green = fromHex(str.substring(2,4));
  this.blue = fromHex(str.substring(4,6));
  this.toString = ColorString;
  return this;
}
function ColorString () {
  return toHex(this.red) + toHex(this.green) + toHex(this.blue);
}
function BodyColor (bgColor,fgColor,linkColor,vlinkColor,alinkColor) {
  this.bgColor = bgColor;
  this.fgColor = fgColor;
  this.linkColor = linkColor;
  this.vlinkColor = vlinkColor;
  this.alinkColor = alinkColor;
  this.toString = BodyColorString;
  return this;
}
function BodyColorString () {
  return '<body' +
    ((this.bgColor == null) ? '' : ' bgcolor="#' + this.bgColor + '"') +
    ((this.fgColor == null) ? '' : ' text="#' + this.fgColor + '"') +
    ((this.linkColor == null) ? '' : ' link="#' + this.linkColor + '"')      
          +
    ((this.vlinkColor == null) ? '' : ' vlink="#' + this.vlinkColor +     
          '"') +
    ((this.alinkColor == null) ? '' : ' alink="#' + this.alinkColor + 
          '"') +
    '>';
}
function Alternator (bodyA, bodyB, text) {
  this.bodyA = bodyA;
  this.bodyB = bodyB;
  this.currentBody = "A";
  this.text = text;
  this.toString = AlternatorString;
  return this;
}
function AlternatorString () {
  var str = "<html>";
  with (this) {
    if (currentBody == "A") {
      str += bodyA;
      currentBody = "B";
    }
    else {
      str += bodyB;
      currentBody = "A";
    }
    str += text + '</body></html>';
  }
  return str;
}
function Event (start, loops, delay, action) {
  this.start = start * 1000;
  this.next = this.start;
  this.loops = loops;
  this.loopsRemaining = loops;
  this.delay = delay * 1000;
  this.action = action;
  return this;
}
function EventQueue (name, delay, loopAfter, loops, stopAfter) {
  this.active = true;
  this.name = name;
  this.delay = delay * 1000;
  this.loopAfter = loopAfter * 1000;
  this.loops = loops;
  this.loopsRemaining = loops;
  this.stopAfter = stopAfter * 1000;
  this.event = new Object;
  this.start = new Date ();
  this.loopStart = new Date();
  this.eventID = 0;
  this.addEvent = AddEvent;
  this.processEvents = ProcessEvents;
  this.startQueue = StartQueue;
  this.stopQueue = StopQueue;
  return this;
}
function AddEvent (event) {
  this.event[this.eventID++] = event;
}
function StartQueue () {
  with (this) {
    active = true;
    start = new Date();
    loopStart = new Date();
    loopsRemaining = loops;
    setTimeout (name + ".processEvents()", this.delay);
  }
}
function StopQueue () {
  this.active = false;
}
function ProcessEvents () {
  with (this) {
    if (!active) return;
    var now = new Date();
    if (now.getTime() - start.getTime() >= stopAfter) {
      active = false;
      return;
    }
    var elapsed = now.getTime() - loopStart.getTime();
    if (elapsed >= loopAfter) {
      if (-loopsRemaining <= 0) {
        active = false;
        return;
      }
      loopStart = new Date();
      elapsed = now.getTime() - loopStart.getTime();
      for (var i in event)
        if (event[i] != null) {
          event[i].next = event[i].start;
          event[i].loopsRemaining = event[i].loops;
        }
    }
    for (var i in event)
      if (event[i] != null)
        if (event[i].next <= elapsed)
          if (event[i].loopsRemaining- > 0) {
            event[i].next = elapsed + event[i].delay;
            eval (event[i].action);
          }
    setTimeout (this.name + ".processEvents()", this.delay);
  }
}
var black = new Color ("000000");
var white = new Color ("FFFFFF");
var blue = new Color ("0000FF");
var magenta = new Color ("FF00FF");
var yellow = new Color ("FFFF00");

var blackOnWhite = new BodyColor (white, black);
var whiteOnBlack = new BodyColor (black, white);
var blueOnWhite = new BodyColor (white, blue);
var yellowOnBlue = new BodyColor (blue, yellow);
var magentaOnYellow = new BodyColor (yellow, magenta);

var flashyText = new Alternator (blackOnWhite, whiteOnBlack,
  '<h1 align="center">Visual Effects</h1>');
var dance1 = new Alternator (yellowOnBlue, magentaOnYellow,
  '<h1 align="center">Dancing...</h1>');
var dance2 = new Alternator (whiteOnBlack, yellowOnBlue,
  '<h1 align="center">Dancing...</h1>');
var dance3 = new Alternator (new BodyColor(black,yellow), magentaOnYellow,
  '<h1 align="center">Dancing...</h1>');
var inthe1 = new Alternator (magentaOnYellow, yellowOnBlue,
  '<h1 align="center">...in the...</h1>');
var inthe2 = new Alternator (blackOnWhite, whiteOnBlack,
  '<h1 align="center">...in the...</h1>');
var inthe3 = new Alternator (yellowOnBlue, blueOnWhite,
  '<h1 align="center">...in the...</h1>');
var streets1 = new Alternator (whiteOnBlack, yellowOnBlue,
  '<h1 align="center">...streets!</h1>');
var streets2 = new Alternator (blueOnWhite, magentaOnYellow,
  '<h1 align="center">...streets!</h1>');
var streets3 = new Alternator (yellowOnBlue, blackOnWhite,
  '<h1 align="center">...streets!</h1>');

var flashEvent = new Event (0, 10, 0.1,
  'self.head.location="javascript:parent.flashyText"');
var d1e = new Event (0, 10, .1,
  'self.f1.location="javascript:parent.dance1"');
var d2e = new Event (5, 10, .1,
  'self.f2.location="javascript:parent.dance2"');
var d3e = new Event (10, 10, .1,
  'self.f3.location="javascript:parent.dance3"');
var i1e = new Event (3, 10, .1,
  'self.f1.location="javascript:parent.inthe1"');
var i2e = new Event (8, 10, .1,
  'self.f2.location="javascript:parent.inthe2"');
var i3e = new Event (13, 10, .1,
  'self.f3.location="javascript:parent.inthe3"');
var s1e = new Event (6, 10, .1,
  'self.f1.location="javascript:parent.streets1"');
var s2e = new Event (11, 10, .1,
  'self.f2.location="javascript:parent.streets2"');
var s3e = new Event (16, 10, .1,
  'self.f3.location="javascript:parent.streets3"');

var evq = new EventQueue ("evq", 0.1, 20, 10, 60);
evq.addEvent (flashEvent);
evq.addEvent(d1e);
evq.addEvent(i1e);
evq.addEvent(s1e);
evq.addEvent(d2e);
evq.addEvent(i2e);
evq.addEvent(s2e);
evq.addEvent(d3e);
evq.addEvent(i3e);
evq.addEvent(s3e);

function initialize () {
  evq.startQueue();
}
// end script ->
</script>
<frameset rows="52,52,52,52,*" onLoad="initialize()">
  <frame name="head" src="javascript:parent.emptyFrame"
     marginwidth=1 marginheight=1 scrolling="no" noresize>
  <frame name="f1" src="javascript:parent.emptyFrame"
     marginwidth=1 marginheight=1 scrolling="no" noresize>
  <frame name="f2" src="javascript:parent.emptyFrame"
     marginwidth=1 marginheight=1 scrolling="no" noresize>
  <frame name="f3" src="javascript:parent.emptyFrame"
     marginwidth=1 marginheight=1 scrolling="no" noresize>
  <frame name="body" src="javascript:parent.emptyFrame">
</frameset>
<noframes>
<h2 align="center">Netscape 2.0 or other JavaScript-enabled browser  required</h2>
</noframes>
</html>

A Color Fader

Like the Alternator effect, the Fader effect involves the transition from one color scheme to another. But instead of jumping abruptly between colors, the Fader displays a series of intermediate shades, creating the illusion of a smooth transition. Although the Alternator effect is noisy and jarring, the Fader effect is calm, serene, even solemn. In particular, a slow fade up from (or down to) black can lend a somber, serious tone to the message being conveyed. Or the Fader can be used to create wild, psychedelic effects-whichever best suits your purpose.

By now, it should come as no surprise that we'll start by creating a new object type. But before we create the Fader object itself, we need to create a special object that calculates an intermediate color value between two Color objects. I'll call this the IntColor object. Its constructor is show in listing 15.12.


Listing 15.12  The IntColor Object Constructor

function IntColor (start, end, step, steps) {
  this.red =
    Math.round(start.red+(((end.red-start.red)/(steps-1))*step));
  this.green =
    Math.round(start.green+(((end.green-start.green)/(steps-1))*step));
  this.blue =
    Math.round(start.blue+(((end.blue-start.blue)/(steps-1))*step));
  this.toString = ColorString;
  return this;
}

The IntColor() constructor takes two Color objects-start and end-plus the number of steps between the start and end colors and the current step. The resultant object is identical to a Color object and can be used as such. It may be convenient to think of IntColor() as just another constructor for a Color object.

Now that we have a way to calculate intermediate colors, we can create our Fader object. Listing 15.13 shows its constructor.


Listing 15.13  The Fader Object Constructor

function Fader (bodyA, bodyB, steps, text) {
  this.bodyA = bodyA;
  this.bodyB = bodyB;
  this.step = 0;
  this.steps = steps;
  this.text = text;
  this.toString = FaderString;
  return this;
}
function FaderString () {
  var intBody = new BodyColor();
  with (this) {
    if (bodyA.bgColor != null && bodyB.bgColor != null)
      intBody.bgColor =
        new IntColor (bodyA.bgColor, bodyB.bgColor, step, steps);
    if (bodyA.fgColor != null && bodyB.fgColor != null)
      intBody.fgColor =
        new IntColor (bodyA.fgColor, bodyB.fgColor, step, steps);
    if (bodyA.linkColor != null && bodyB.linkColor != null)
      intBody.linkColor =
        new IntColor (bodyA.linkColor, bodyB.linkColor, step, steps);
    if (bodyA.vlinkColor != null && bodyB.vlinkColor != null)
      intBody.vlinkColor =
        new IntColor (bodyA.vlinkColor, bodyB.vlinkColor, step, steps);
    if (bodyA.alinkColor != null && bodyB.alinkColor != null)
      intBody.alinkColor =
        new IntColor (bodyA.alinkColor, bodyB.alinkColor, step, steps);
    step++;
    if (step >= steps)
      step = 0;
  }
  return '<html>' + intBody + this.text + '</body></html>';
}

The Fader object itself is similar in construction to the Alternator object. The Fader() constructor takes a beginning BodyColor object (bodyA), an ending BodyColor object (bodyB), and a text string containing the HTML and text to be displayed. In addition, the Fader() constructor takes the number of steps to be used in the transition from the beginning colors to the ending colors.

The toString() method, FaderString(), is a bit more complex than its Alternator counterpart. It creates a temporary BodyColor object, and populates it with IntColor objects for each color attribute that is present in both the beginning and ending BodyColor objects. It then increments the current step. When all steps have been completed, it resets the current step to zero, so the object can be reused. It returns the specified text, along with an embedded BODY tag generated from the temporary BodyColor object.

It may have occurred to you that a Fader object with steps set to 2 performs exactly the same function as an Alternator object. However, the code is a little longer and involves more processing.

TIP
If you are using both Alternator and Fader objects, you can use a Fader object with two steps in place of an Alternator object and omit the alternator code to save space.

A Fader object is defined in much the same way as an Alternator object, as shown in listing 15.14.


Listing 15.14  Using the Fader Object

var fadingText = new Fader (yellowOnBlue, magentaOnYellow, 10,
  '<h1 align="center">Visual Effects</h1>');
var evq = new EventQueue ("evq", 0.1, 20, 10, 60);
evq.addEvent (new Event (0, 10, 0.1,
  'self.head.location="javascript:parent.fadingText"'));

Notice that instead of creating a named variable for our Fader event, we defined it in the parameter list for the addEvent() method. If you have a lot of events, making up names for them can be a chore-not to mention a source of confusion.

When creating events for Fader objects, it's important to remember that the number of loops specified for the event should normally be the same as the number of steps in the fade. If you specify a smaller number of loops, you'll get an incomplete fade; specify a larger number, and the fade will start over with the initial color.

A Scrolling Marquee

By now, you've probably seen dozens of pages with a scrolling text ticker down at the bottom in the status area. Besides being hard to read, these tend to block out the usual status messages associated with cursor actions. The Java applet marquees and tickers are much better, but they take awhile to load and won't run on all platforms. However, you can enjoy the best of both worlds by creating a JavaScript marquee that's both readable and quick to load.

Ideally, our marquee should be able to display text in a variety of fonts, sizes, and colors, in any combination. So before we define the Marquee object itself, let's create some text-handling objects that will help us do just that. The Text and Block object constructors are shown in listing 15.15.


Listing 15.15  The Text and Block Object Constructors

function Text (text, size, format, color) {
  this.text = text;
  this.length = text.length;
  this.size = size;
  this.format = format;
  this.color = color;
  this.toString = TextString;
  this.substring = TextString;
  return this;
}
function TextString (start, end) {
  with (this) {
    if (TextString.arguments.length < 2 || start >= length) start = 0;
    if (TextString.arguments.length < 2 || end > length) end = length;
    var str = text.substring(start,end);
    if (format != null) {
      if (format.indexOf("b") >= 0) str = str.bold();
      if (format.indexOf("i") >= 0) str = str.italics();
      if (format.indexOf("f") >= 0) str = str.fixed();
    }
    if (size != null) str = str.fontsize(size);
    if (color != null) {
      var colorstr = color.toString(); // Note: Netscape 2.0 bug  workaround
      str = str.fontcolor(colorstr);
    }
  }
  return str;
}
function Block () {
  var argv = Block.arguments;
  var argc = argv.length;
  var length = 0;
  for (var i = 0; i < argc; i++) {
    length += argv[i].length;
    this[i] = argv[i];
  }
  this.length = length;
  this.entries = argc;
  this.toString = BlockString;
  this.substring = BlockString;
  return this;
} 
function BlockString (start, end) {
  with (this) {
    if (BlockString.arguments.length < 2 || start >= length) start = 0;
    if (BlockString.arguments.length < 2 || end > length) end = length;
  }
  var str = "";
  var segstart = 0;
  var segend = 0;
  for (var i = 0; i < this.entries; i++) {
    segend = segstart + this[i].length;
    if (segend > start)
      str += this[i].substring(Math.max(start,segstart)-segstart,
        Math.min(end,segend)-segstart);
    segstart += this[i].length;
    if (segstart >= end)
      break;
  }
  return str;
}

The Text object is used to contain a string, along with font, size, and color information. If you look closely, you'll see that the Text object has some interesting properties, both figuratively and literally.

The Text object is designed to mimic JavaScript strings, but with some important differences. The Text object has a length property, for instance, and a substring() method. But while the length property returns the length of the text itself, the substring() method returns the requested substring plus the HTML tags required to render the substring in the desired font, size, and color.

Why is this important? Because the Marquee object must display segments of text to produce its scrolling effect; so to maintain proper formatting, it needs to be able to retrieve substrings as small as a single character with all their HTML attributes intact.

The Text() constructor takes a text string, and, optionally, a font size, a Color object, and a format string. The format string can contain the lowercase letters b, i, or f, or any combination of the three, which stand for bold, italic, and fixed, respectively. The Color object specifies the foreground color to be used when displaying the text.

NOTE
Due to a JavaScript bug in Netscape 2.0, font size must be passed as a string (e.g., "7"), rather than a number, when specified as a parameter to the Text() constructor.

The Block object is used to combine two or more Text objects, JavaScript strings, or even other Block objects in any combination. Like the Text object, the Block object mimics JavaScript string behavior. A call to its substring() method might return portions of several of its constituent objects, with all their HTML formatting intact.

The Block() constructor accepts any number of Text, string, or Block objects. These can be considered to be logically concatenated in the order specified in the argument list.

Listing 15.16 shows an example of using Text and Block objects.


Listing 15.16  Using Text and Block Objects

var t1 = new Text ("When shall ", "5", "", blue);
var t2 = new Text ("we three ", "6", "fb", red);
var t3 = new Text ("meet again, ", "5", "bfi", yellow);
var t4 = new Text ("or in rain? ", "6", "ib", red);
var b1 = new Block (t3, "In thunder, lightning, ", t4);
var b2 = new Block (t1, t2, b1);

A call to b2.substring(5,25) would then return the following:

<FONT COLOR="#0000FF"><FONT SIZE="5">shall </FONT></FONT>
<FONT COLOR="#FF0000"><FONT SIZE="6"><TT><B>we three </B></TT></FONT></FONT>
<FONT COLOR="#FFFF00"><FONT SIZE="5"><I><B>meet </B></I></FONT></FONT>
Your

Using Text and Block objects, you can create marquees in a wide variety of styles. Now let's take a look at the Marquee object itself. Listing 15.17 shows its constructor.


Listing 15.17  The Marquee Object Constructor

function Marquee (body, text, maxlength, step) {
  this.body = body;
  this.text = text;
  this.length = text.length;
  this.maxlength = maxlength;
  this.step = step;
  this.offset = 0;
  this.toString = MarqueeString;
  return this;
}
function MarqueeString () {
  with (this) {
    var endstr = offset + maxlength;
    var remstr = 0;
    if (endstr > text.length) {
      remstr = endstr - text.length;
      endstr = text.length;
    }
    var str = nbsp(text.substring(offset,endstr) +
      ((remstr == 0) ? "" : text.substring(0,remstr)));
    offset += step;
    if (offset >= text.length)
      offset = 0;
    else if (offset < 0)
      offset = text.length - 1;
  }
  return '<html>' + this.body + '<table border=0 width=100%  height=100%><tr>' +
    '<td align="center" valign="center">' + 
	str + '</td></tr></table></ body></html>';
}
function nbsp (strin) {
  var strout = "";
  var intag = false;
  var len = strin.length;
  for(var i=0, j=0; i < len; i++) {
    var ch = strin.charAt(i);
    if (ch == "<")
      intag = true;
    else if (ch == ">")
      intag = false;
    else if (ch == " " && !intag) {
      strout += strin.substring(j,i) + "&nbsp";
      j = i + 1;
    }
  }
  return strout + strin.substring(j,len);
}

The body parameter to the Marquee() constructor accepts a BodyColor object. This object determines the overall color scheme for the marquee.

The text parameter can be a Block object, a Text object, or a JavaScript string object. The text produced by this object will be scrolled across the screen to create the marquee effect. Any colors embedded in this object will override the foreground color specified in the body parameter for the corresponding section of text.

The maxlength parameter is the maximum length of the text returned by the Marquee object, not counting HTML formatting tags. You will need to experiment with this a bit to get the right width. A good starting point is to use the width of the marquee frame divided by ten. So for a 400-pixel-wide window, start with 40 and then adjust as necessary. It's okay to specify a length slightly larger than the frame width, but if you specify a much longer length, it will slow down processing and increase memory usage.

The step parameter specifies the number of characters the marquee will scroll each time it is invoked. You will generally want to set this to 1 or 2, or, to scroll backwards, -1 or -2. Combined with the delay time defined for the Marquee event, the step parameter determines how fast the Marquee scrolls across the screen.

The toString() method, MarqueeString(), uses a table to center the text vertically and horizontally within the frame. (Depending on how you use the Alternator and Fader objects, you may want to modify their toString() methods to do this as well.) Note that if you use a combination of large and small fonts in your marquee, the text may "wobble" vertically during the transition from one size to another.

The nbsp() function is used to replace all space characters with nonbreaking spaces (&nbsp). This enables you to include consecutive spaces (normally ignored by HTML) in your text. It also prevents the scrolling text from breaking into two or more lines when the font is small enough or the marquee window large enough that this would otherwise occur.

In listing 15.18, we create a marquee, using the opening lines from Shakespeare's Macbeth for our text.


Listing 15.18  Using the Marquee Object

var mbScene = new Block (
  new Text ("When shall we three meet again, ", "5", "b", red),
  new Text ("In thunder, lightning, or in rain? ", "6", "bf", blue),
  new Text ("When the hurlyburly\'s done, ", "5", "ib", yellow),
  new Text ("When the battle\'s lost and won. ", "6", "bfi", magenta),
  new Text ("That will be ere the set of sun. ", "6", "fb", red),
  "................"
  );

var mbMarquee = new Marquee (whiteOnBlack, mbScene, 50, 2);
var evq = new EventQueue ("evq", 0.1, 120, 5, 600);
evq.addEvent (new Event (0, mbMarquee.length * 3, 0.125,
  'self.f1.location = "javascript:parent.mbMarquee"'));

There are several points to note in this example. First, rather than define a separate named variable for each Text object, I created them in the parameter list for the Block constructor. Again, this is usually preferable to cluttering your program with a lot of variables that are only referenced once.

Next, notice that I escaped the apostrophes in the text using the \ character. It's sometimes easy to forget to do this when you're working with real-world text in JavaScript applications.

The line of dots at the end of the Block acts as a separator between the end of the text and the beginning when the marquee wraps around. In this particular case, it would have been better to use a Text object with a larger font size because the rest of the text in the block uses larger fonts. But the point to keep in mind is that you can use plain strings in Block objects if you want to.

Finally, when creating the Event for the marquee, the number of loops is specified as a multiple of the length of the Marquee object. This is much easier than counting all the characters in the Block object manually!

The complete code for this example is included on the accompanying CD (see 15exm02.htm). Its output is shown in figure 15.5.

Figure 15.5 : A scrolling marquee can include multiple font styles, colors, and sizes.

The Static Object

In some cases, you may just want to put some text in a frame at a particular time. This isn't really an effect, per se, but it would be convenient to have an object similar to the rest of our objects for this purpose. The Static object fills this need. Its constructor is shown in listing 15.19.


Listing 15.19  The Static Object Constructor

function Static (body, text) {
  this.body = body;
  this.text = text;
  this.toString = StaticString;
  return this;
}
function StaticString () {
  return '<html>' + this.body + this.text + '</body></html>';
}

The Static() constructor takes a BodyColor object and a text string, which may contain HTML. You could also use a Text object or a Block object for the text parameter. The following is an example of using the Static object:

var beHere = new Static (blackOnWhite, '<h1 align="center">Be Here Now</h1>');
var evq = new EventQueue ("evq", 0.1, 120, 5, 600);
evq.addEvent (new Event (12, 1, 10,
  'self.f4.location = "javascript:parent.beHere"'));

Animating Images

The best way to animate images using JavaScript is not to. Netscape 2.0 supports GIF89a multipart images, which contain built-in timing and looping instructions. These load faster and run more smoothly than animation created using JavaScript and can be placed anywhere on the page (whereas JavaScript animation currently require their own frame). A number of inexpensive shareware utilities are available for creating GIF animation, the best-known of which is probably GIF Construction Set by Alchemy Mindworks. While GIF89a images are currently supported only by Netscape, it's pretty safe to assume that when other browsers support JavaScript, they'll also support GIF animation.

All that said, there may be cases when you want or need to create an animation using JavaScript. In particular, you may want to do so when you're creating images on-the-fly, a subject that will be treated in depth in the next section.

Before we create our Animator object, we'll need an object to hold information about individual images. Listing 15.20 shows the constructor for the Image object.


Listing 15.20  The Image Object Constructor

function Image (url, width, height) {
  this.url = url;
  this.width = width;
  this.height = height;
  return this;
}

The url parameter to the Image() constructor must be a fully specified URL; relative URLs won't work within the framework we've developed because the default protocol is always assumed to be javascript:. The width and height parameters are required, but don't necessarily have to be accurate: Netscape automatically scales images to the width and height specified.

Now let's take a look at our Animator object. Its constructor is shown in listing 15.21.


Listing 15.21  The Animator Object Constructor

function Animator (name, body) {
  var argv = Animator.arguments;
  var argc = argv.length;
  for (var i = 2; i < argc; i++)
    this[i-2] = argv[i];
  this.name = name;
  this.body = body;
  this.images = argc - 2;
  this.image = 0;
  this.ready = "y";
  this.toString = AnimatorString;
  return this;
}
function AnimatorString () {
  var bodystr = this.body.toString();
  var bodystr = bodystr.substring(0, bodystr.length - 1) +
    ' onLoad="parent.' + this.name + '.ready=\'y\'">';
  var str = '<html>' + bodystr +
    '<table border=0 width=100% height=100%><tr><td align="center" valign="center">' +
    '<img src="' + this[this.image].url + '" width=' +  this[this.image].width +
    ' height=' + this[this.image].height + '></td></table></body></ html>';
  this.image++;
  if (this.image >= this.images)
    this.image = 0;
  this.ready = "n";
  return str;
}

The Animator() constructor takes a name parameter, which must be the same as the variable name assigned to the Animator object. This is followed by a BodyColor object, and any number of Image objects. You will generally want to create your Image objects in the parameter list for the Animator() constructor.

Unlike the Color, Text, and other objects we've used in our effects so far, images are not immediately available when we want to put them on the screen-they are usually loaded from a server. And there's no reliable way of guessing how long that will take. If you try to display them on a fixed timetable, most likely none of them would get a chance to load completely: the next image you try to display would clobber the one currently loading, and Netscape would start loading it again from scratch the next time it was called for.

The only way to get around this is to let each image load completely before displaying the next image. The Animator object does this by including an onLoad handler in the BODY tag for each image it writes to the screen. When the Animator object's toString() method, AnimatorString(), generates a new frame, it sets the ready flag in the object to n, meaning that the Animator is not ready to display a new frame. Once the image has loaded completely, the onLoad handler is called and sets the ready flag back to y. (This is the reason you need to specify the name of the Animator object: so the onLoad handler knows which object to update.)

The last part of this trick falls to the Event object we create for the Animator. You may recall that an Event's action can be any valid JavaScript statement, so simply include an if statement in the action to test whether the Animator is ready before updating the frame. Listing 15.22 shows an example of using the Animator. Figure 15.6 shows the output.

Figure 15.6 : A logo can be animated using the Animator object.


Listing 15.22  Using the Animator Object

var anim = new Animator ("anim", blackOnWhite,
  new Image ("http://www.hidaho.com/colorcenter/img/logo1.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo2.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo3.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo4.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo5.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo6.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo7.gif", 32, 32),
  new Image ("http://www.hidaho.com/colorcenter/img/logo8.gif", 32, 32)
  );
var evq = new EventQueue ("evq", 0.1, 120, 5, 600);
evq.addEvent (new Event (0, 60, 0.1,
  'if (anim.ready=="y") self.f1.location="javascript:parent.anim"'));

NOTE
Due to a JavaScript bug in Netscape 2.0, in some cases it is not possible to read or set boolean or numeric values across frames. This is why the Animator object uses a string for the ready flag.

Generating Images

Loading images from a server has its limitations. Apart from the amount of time this can take-especially over a slow connection-you generally have a fixed set of images to work with (unless you generate images on the server using a CGI program). There are times when it is useful to create images on-the-fly, perhaps in response to user input or to create a dynamic animation.

JavaScript offers two solutions. The first is to use single-pixel GIF files to construct images. Because Netscape automatically scales images to the specified width and height, you can create rectangles of various dimensions from a 1 ¥ 1 GIF image of a particular color. This technique is especially useful for creating dynamic bar charts; but beyond that, its applications are very limited. I won't cover this technique here, but I encourage you to experiment with it on your own.

The second solution is to generate XBM-format images. You may not have heard of these before, but you've probably seen them. They're often used as icons in server directory listings. You are most likely to see one when downloading a file via FTP.

The greatest drawback to XBM images is that they're monochromatic-in other words, black-and-white, though Netscape renders them as black-and-gray. But this is also something of an advantage to us because they can be represented internally as a string of bits, one per pixel, on or off. This also makes manipulating them fairly straightforward and not too costly in terms of processor cycles-an important consideration when working with an interpreted language, such as JavaScript. Also important to us, the XBM file's native format is ASCII text, which can be represented using JavaScript strings.

The XBM Format

An XBM image consists of a header specifying its width and height in pixels and a string of hexadecimal byte codes. As shown in listing 15.23, it looks a lot like something you'd find in a C-language source file.


Listing 15.23  An XBM Image File Header

#define xbm_width 32
#define xbm_height 32
static char xbm_bits[] = {
  0xFF,0x02,0x88,0x25,0x3C,0xB4,0x11,0xDB,
...
};

The names xbm_width, xbm_height, and xbm_bits are not part of the specification. We chose these because they are descriptive, but the names could be any valid C-style identifiers-it's the format that's important.

Each byte code is a bitmap corresponding to eight pixels in a row of pixels. The first byte code represents the upper-leftmost eight pixels in an image. Bits are processed from left to right until the specified width is reached. The next set of bits then defines the next row of pixels, and so on, until the entire image is drawn.

Representing an XBM Image

We'll represent the XBM bits internally as JavaScript numbers. Because JavaScript uses 32-bit integers, it makes sense to store 32 XBM bits in each JavaScript number. However, it turns out that the high-order sign bit can't be set on some platforms, so we'll use 16-bit numbers instead. This wastes some space, but the math is much easier if we stick with powers of 2.

Our XBM images will be made up of two types of objects: the xbmRow object, which contains an array of 16-bit numbers, and the xbmImage object, which contains an array of xbmRow objects. These objects both contain additional information used in manipulating the image and in translating it to ASCII text for display. Listing 15.24 shows their constructors.


Listing 15.24  The xbmRow and xbmImage Object Constructors

function xbmRow (parent, columns, initialValue) {
  this.redraw = true;
  this.text = null;
  this.parent = parent;
  this.col = new Object();
  for (var i = 0; i < columns; i++)
    this.col[i] = initialValue;
  this.toString = xbmRowString;
  return this;
}
function xbmImage (width, height, initialValue) {
  this.width = (width+15)>>4;
  this.pixelWidth = this.width<<4;
  this.height = height;
  this.head = "#define xbm_width " + (this.pixelWidth) +
    "\n#define xbm_height " + this.height +
    "\nstatic char xbm_bits[] = {\n";
  this.initialValue = ((initialValue == null) ? 0 : initialValue);
  this.negative = false;
  this.row = new Object();
  for (var i = 0; i < height; i++)
    this.row[i] = new xbmRow(this, this.width, this.initialValue);
  this.drawPoint = xbmDrawPoint;
  this.drawLine = xbmDrawLine;
  this.drawRect = xbmDrawRect;
  this.drawFilledRect = xbmDrawFilledRect;
  this.drawCircle = xbmDrawCircle;
  this.drawFilledCircle = xbmDrawFilledCircle;
  this.reverse = xbmReverse;
  this.clear = xbmClear;
  this.partition = xbmPartitionString;
  this.toString = xbmString;
  return this;
}

The xbmImage() constructor takes the width and height of the image, in pixels, as parameters. An optional initial value can also be specified-if supplied; this will create a pattern of vertical lines in the image. Otherwise, a zero is assumed, which results in a blank image.

The xbmImage() constructor calls the xbmRow() constructor to create each row in the image. xbmRow() should be considered an internal function. You don't need to call it directly.

Both xbmImage and xbmRow have toString() methods: xbmImageString() and xbmRowString(), respectively. These create the ASCII representation of the XBM image when it's time to display it. A third method, xbmPartitionString(), optimizes the string-building process, which would otherwise consume an excessive amount of memory. These are shown in listing 15.25.


Listing15.25  The xbmImage toString() Methods

function xbmRowString () {
  if (this.redraw) {
    this.redraw = false;
    this.text = "";
    for (var i = 0; i < this.parent.width; i++) {
      var pixels = this.col[i];
      if (this.parent.negative)
        pixels ^= 0xFFFF;
      var buf = "0x" + hexchars.charAt((pixels>>4)&0xF) +
        hexchars.charAt(pixels&0xF) + ",0x" +
        hexchars.charAt((pixels>>12)&0xF) +
        hexchars.charAt((pixels>>8)&0xF) + ",";
      this.text += buf;
    }
  }
  return this.text;
}
function xbmPartitionString (left,right) {
  if (left == right) {
    var str = this.row[left].toString();
    if (left == 0)
      str = this.head + str;
    else if (left == this.height - 1)
      str += "};\n";
    return str;
  }
  var mid = (left+right)>>1;
  return this.partition(left,mid) + this.partition(mid+1,right);
}
function xbmString () {
  return this.partition(0,this.height - 1);
}

XBM Drawing Methods

The foundation of our XBM drawing capability is the drawPoint() method, xbmDrawPoint(), shown in listing 15.26. As all of our XBM drawing methods, the drawPoint() method doesn't actually draw anything on the screen. Instead, it updates the internal state of the xbmImage object to indicate that the specified point needs to be drawn.


Listing 15.26  The xbmDrawPoint() Function

function xbmDrawPoint (x,y) {
  if (x < 0 || x >= this.pixelWidth ||
      y < 0 || y >= this.height)
    return;
  this.row[y].col[x>>4] |= 1<<(x&0xF);
  this.row[y].redraw = true;
}

The drawPoint() method takes the x and y coordinates of the point to be drawn. These are specified relative to the upper-left corner of the image, which is point (0,0).

The y coordinate is used as an index into the array of xbmRow objects. The high-order bits of the x coordinate are used to compute an index into the array of JavaScript numbers representing the row. The low-order bits are then used to calculate the bit offset for the desired pixel coordinate, which is turned on.

Let's create an xbmImage object and draw a point:

var picture = new xbmImage (64,64);
picture.drawPoint(10,15);

Drawing points can be useful for creating fine detail within an image, but it would take a lot of drawPoint() calls to create a useful image. Fortunately, we have some more powerful drawing methods at our disposal.


Listing 15.27  The xbmDrawLine() Function

function xbmDrawLine (x1,y1,x2,y2) {
  var x,y,e,temp;
  var dx = Math.abs(x1-x2);
  var dy = Math.abs(y1-y2);
  if ((dx >= dy && x1 > x2) || (dy > dx && y1 > y2)) {
    temp = x2;
    x2 = x1;
    x1 = temp;
    temp = y2;
    y2 = y1;
    y1 = temp;
  }
  if (dx >= dy) {
    e = (y2-y1)/((dx == 0) ? 1 : dx);
    for (x = x1, y = y1; x <= x2; x++, y += e)
      this.drawPoint(x,Math.round(y));
  }
  else {
    e = (x2-x1)/dy;
    for (y = y1, x = x1; y <= y2; y++, x += e)
      this.drawPoint(Math.round(x),y);
  }
}

The drawLine() method, xbmDrawLine(), shown in listing 15.27, draws a line between two points by making a series of calls to drawPoint(). It takes two pairs of coordinates, (x1,y1) and (x2,y2), as parameters. The algorithm is reasonably efficient, at least in the context of an interpreted language. The drawLine() method forms the basis of our rectangle-drawing algorithms, shown in listing 15.28.


Listing 15.28  Rectangle Drawing Functions

function xbmDrawRect (x1,y1,x2,y2) {
  this.drawLine (x1,y1,x2,y1);
  this.drawLine (x1,y1,x1,y2);
  this.drawLine (x1,y2,x2,y2);
  this.drawLine (x2,y1,x2,y2);
}
function xbmDrawFilledRect (x1,y1,x2,y2) {
  var x,temp;
  if (x1 > x2) {
    temp = x2;
    x2 = x1;
    x1 = temp;
    temp = y2;
    y2 = y1;
    y1 = temp;
  }
  for (x = x1; x <= x2; x++)
    this.drawLine(x,y1,x,y2);
}

The drawRect() method, xbmDrawRect(), draws a hollow rectangle, given two
opposing corner coordinate pairs, (x1,y1) and (xy,y2). The drawFilledRect() method, xbmDrawFilledRect(), draws a filled rectangle, as you probably guessed.

Let's draw some lines and rectangles. Figure 15.7 shows the results.

Figure 15.7 : Lines and rectangles drawn using the xbmImage object.

var picture = new xbmImage (64,64);
picture.drawLine (0,0,63,63);
picture.drawRect (32,0,63,32);
picture.drawFilledRect (0,32,32,63);

Our last two drawing methods, shown in listing 15.29, draw hollow and filled circles.


Listing 15.29  Circle Drawing Functions

function xbmDrawCircle (x,y,radius) {
  for (var a=0, b=1; a < b; a++) {
    b = Math.round(Math.sqrt(Math.pow(radius,2)-Math.pow(a,2)));
    this.drawPoint(x+a,y+b);
    this.drawPoint(x+a,y-b);
    this.drawPoint(x-a,y+b);
    this.drawPoint(x-a,y-b);
    this.drawPoint(x+b,y+a);
    this.drawPoint(x+b,y-a);
    this.drawPoint(x-b,y+a);
    this.drawPoint(x-b,y-a);
  }
}
function xbmDrawFilledCircle (x,y,radius) {
  for (var a=0, b=1; a < b; a++) {
    b = Math.round(Math.sqrt(Math.pow(radius,2)-Math.pow(a,2)));
    this.drawLine(x+a,y+b,x+a,y-b);
    this.drawLine(x-a,y+b,x-a,y-b);
    this.drawLine(x+b,y+a,x+b,y-a);
    this.drawLine(x-b,y+a,x-b,y-a);
  }
}

The drawCircle() method, xbmDrawCircle(), and the drawFilledCircle() method, xbmDrawFilledCircle(), take the coordinates of the center point of the circle plus the radius. These methods take advantage of the fact that it's necessary only to compute the points for a single octant (one-eighth) of a circle. They compute these points relative to an origin of (0,0), and then translate them to the eight octants relative to the x and y coordinates.

Because the drawPoint() method automatically "clips" any points that don't lie within the image area, we can draw circles that only partially intersect our image (as shown in figure 15.8):

Figure 15.8 : Circle and arc drawn using the xbmImage object.

var picture = new xbmImage (64,64);
picture.drawCircle (32,32,20); // completely within image
picture.drawCircle (0,0,30); // only 90 degrees of arc appear

Displaying Generated Images

A generated xbmImage object can be displayed in much the same way that an ordinary image would be displayed, except that it has a javascript: URL. Listing 15.30 shows an example of using the Static object to supply the surrounding HTML.


Listing 15.30  Displaying an xbmImage Object

picture = new xbmImage (64,64);
picture.drawLine (0,0,63,63);
picture.drawLine (0,63,63,0);
var pictureFrame = new Static (whiteOnBlack,
  '<img src="javascript:parent.picture" width=64 height=64>');
self.frameA.location = "javascript:parent.pictureFrame";

Note, however, that once an xbmImage has been displayed, any subsequent changes to it will not be displayed when you redraw the frame. Netscape assumes that images of a given name don't change, so it uses its cached copy after the first draw. The workaround is to assign the xbmImage object to an object with a different name and then redraw it. The JS-Draw application, shown in the next section, uses an array for this purpose.

You can animate a series of xbmImages using the Animator object we created earlier. Just specify the javascript: URL of the image in the Image object.

A Drawing Application: JS-Draw

JS-Draw is a drawing application based on the xbmImage object and its methods. A couple of methods have been added to clear the image and to display it in negative (white on black).

The complete code is included on the accompanying CD (see 15exm03.htm). An example of its output is shown in figure 15.9.

Figure 15.9 : The JS-Draw application was built using xbmImage() objects.

The Show

The final example, included on the accompanying CD (see 15exm04.htm), uses every trick in the book-or at least, every trick in this chapter.

While it is a little too busy to make a good Web page, you might think of it as a laboratory, a starting point for your ongoing experiments with visual effects.