reptile7's JavaScript blog
Sunday, April 27, 2014
 
Character Interlopers
Blog Entry #318

We have one more auxiliary function in the Color Gradient Text script to cover.

The interpolate( ) function

At the end of our last episode we had determined "lower color" (lci) and "higher color" (hci) indexes for the characters of the ABCDEFG string as regards the ff0000 ffffff 0000ff color spectrum via the lowcolorindex( ) and hicolorindex( ) functions. Returning to the gradient( ) function, the lowcolorindex( )/hicolorindex( ) calls are followed by three statements

rr = Math.round(interpolate(lci / (numcolors - 1), colors.codes[lci].r, hci / (numcolors - 1), colors.codes[hci].r, i / (numchars - 1)));
gg = Math.round(interpolate(lci / (numcolors - 1), colors.codes[lci].g, hci / (numcolors - 1), colors.codes[hci].g, i / (numchars - 1)));
bb = Math.round(interpolate(lci / (numcolors - 1), colors.codes[lci].b, hci / (numcolors - 1), colors.codes[hci].b, i / (numchars - 1)));


that call on an interpolate( ) function

function interpolate(x1, y1, x3, y3, x2) { if (x3 == x1) return y1; else return (x2 - x1) * (y3 - y1) / (x3 - x1) + y1; }

to determine new rgb(rr, gg, bb) color values for the ABCDEFG characters. (I warned you this would get hairy, didn't I?)

The interpolate( ) function is where all of the script's data comes together; it tells us whether the rr/gg/bb values change or don't change, and if they change by how much, as we move along the character and color axes.



Five arguments are passed to the interpolate( ) function, in source order:
(0) lci / (numcolors - 1), which is renamed x1;
(1) colors.codes[lci].(r|g|b), which is renamed y1;
(2) hci / (numcolors - 1), which is renamed x3;
(3) colors.codes[hci].(r|g|b), which is renamed y3; and
(4) i / (numchars - 1), which is renamed x2.

Theoretical analysis

The y1 values represent a starting thecolors color and the y3 values represent the following thecolors color, e.g., red and white, respectively; the y3 - y1 term in the interpolate( ) function therefore represents a thecolors color distance that we will traverse over the course of one or more for (i = 0; i < numchars; ++i) { ... } loop iterations.

The x2 variable measures the total movement along the character axis since the beginning of the loop, i.e., the total movement from the axis origin at the A character, whereas the x1 variable measures the corresponding movement along the color axis. At the same time, the x1 variable is a stand-in for the previous (most recently encountered) 'station' along the color axis whereas the x3 variable is a stand-in for the following station; when a character horizontally lines up with a station, x1 and x3 are equal. In the interpolate( ) function, the x2 - x1 term measures the movement along the character axis from the previous color axis station whereas the x3 - x1 term serves as a coefficient that converts the x2 - x1 distance to the corresponding distance along the color axis.

In practice

For the i = 0 loop iteration there's no movement along either axis, as noted earlier. As the A character's lci and hci indexes are both 0, x1 and x3 are equal and therefore the y1 r/g/b values for the colors.codes[0] station - 255, 0, and 0 - are returned and respectively assigned to rr, gg, and bb.

Over the next three iterations we will go from the ff0000 thecolors color to the ffffff thecolors color, or decimally a y3 - y1 transition from rgb(255, 0, 0) to rgb(255, 255, 255).

For the i = 1 iteration, we move ⅙ of the way along the character axis but there's no movement along the color axis in that the B character hasn't gotten to the colors.codes[1] station yet. As lci is 0 and hci is 1, the y1 arguments are per the interpolate( ) else clause shifted by (y3 - y1) × ⅙ ÷ ½ to give 255, 85, and 85 values for rr, gg, and bb, respectively.

For the i = 2 iteration and C character, we are ⅓ of the way along the character axis but are still stuck at the colors.codes[0] station as regards the color axis. As for B, C's lci is 0 and hci is 1, and therefore the y1s are shifted by (y3 - y1)*2/3 to give 255, 170, and 170 values for rr, gg, and bb, respectively.

The i = 3 iteration and D character bring us to the midpoints of the character and color axes. As D's lci and hci are both 1, the colors.codes[1] station's 255, 255, and 255 values are assigned to rr, gg, and bb.

Over the last three iterations we go from the ffffff thecolors color to the 0000ff thecolors color, or decimally a y3 - y1 transition from rgb(255, 255, 255) to rgb(0, 0, 255).

Let's pick up the pace:
• For the i = 4 iteration and E character, we are ⅔ of the way along the character axis and halfway along the color axis; as lci is 1 and hci is 2, the y1s are shifted to 170, 170, and 255, which are respectively assigned to rr, gg, and bb.
• For the i = 5 iteration and F character, we are ⅚ of the way along the character axis and halfway along the color axis; lci and hci are still 1 and 2, respectively, and the y1s are shifted to 85, 85, and 255, which are respectively assigned to rr, gg, and bb.
• The i = 6 iteration and G character bring us to the ends of the character and color axes; G's lci and hci are both 2 and the colors.codes[2] station's 0, 0, and 255 values are respectively assigned to rr, gg, and bb.

You may have noticed that in this example we didn't need to Math.round( ) the interpolate( ) returns, but let me assure you that the round( ) operations are indeed necessary in almost all other cases (without getting into the details, their removal doesn't bring the script to a halt but does change the script's effect, and not for the better).

An alternative interpolate( )

The interpolate( ) code does not really require its own function and could use some major help in the readability department. Here's how I would recast it:

var numcolortransitions = numcolors - 1;
var numchartransitions = numchars - 1;
var lowcolor = colors.codes[lci];
var hicolor = colors.codes[hci];
var charCoord = i / numchartransitions; // x2 in the original script
var colorCoord = lci / numcolortransitions; // x1 in the original script
var char_to_colorAxisConvert = (charCoord - colorCoord) * numcolortransitions; // (x2 - x1) / (x3 - x1) in the original script
rr = lci == hci ? lowcolor.r : Math.round(lowcolor.r + (hicolor.r - lowcolor.r) * char_to_colorAxisConvert);
gg = lci == hci ? lowcolor.g : Math.round(lowcolor.g + (hicolor.g - lowcolor.g) * char_to_colorAxisConvert);
bb = lci == hci ? lowcolor.b : Math.round(lowcolor.b + (hicolor.b - lowcolor.b) * char_to_colorAxisConvert);


Regarding the char_to_colorAxisConvert definition, hci - lci is 1 when hci and lci are not equal and therefore 1 / (x3 - x1) equals numcolortransitions.

To recap, here are the interpolate( )d values:

A: 255, 0, 0
B: 255, 85, 85
C: 255, 170, 170
D: 255, 255, 255
E: 170, 170, 255
F: 85, 85, 255
G: 0, 0, 255


And just how do we make use of the rr/gg/bb values? All will be revealed in the next entry, which will definitely conclude our discussion of the Color Gradient Text script.

Friday, April 18, 2014
 
Stations of Color
Blog Entry #317

Welcome back to our analysis of the Java Goodies "Color Gradient Text" script. We will now go through the rest of the gradient( ) function and thereby apply a thecolors color spectrum to some thetext text. For the discussion that follows, we will work with
(a) a simplified ABCDEFG thetext string,
(b) the first gradient( ) call's ff0000 ffffff 0000ff thecolors string, and
(c) the colors.codes[c] data structure as opposed to the split( ) structure I gave you at the end of the previous post.

A graphical view of the effect

For the thetext and thecolors strings given above, imagine a character axis running from A to G and a parallel color axis running from colors.codes[0] to colors.codes[2]:



The character axis comprises seven characters and six character-to-character transitions whereas the color axis comprises three colors and two color-to-color transitions. We will create the script's gradient effect by merging these axes.

You can see that the A character will have the colors.codes[0] color, the D character will have the colors.codes[1] color, and the G character will have the colors.codes[2] color, i.e., the rendered A, D, and G will be pure red, pure white, and pure blue, respectively.

With respect to the color axis, the B and C characters are waypoints along the colors.codes[0]-to-colors.codes[1] transition whereas the E and F characters are waypoints along the colors.codes[1]-to-colors.codes[2] transition. The rendered B will be a reasonably dark but not pure red and the rendered C will be a pale red; complementarily, the rendered E will be a pale blue and the rendered F will be a reasonably dark but not pure blue.

ABCDEFG

gradient( ) guts

The gradient( ) function sets the thetext character colors with the help of three external functions - lowcolorindex( ), hicolorindex( ), and interpolate( ) - whose code is not so easy to grok: I'll explain it as best I can.

With the colors data structure in hand, gradient( ) registers
(a) the number of colors represented by the colors object and
(b) the number of characters composing the thetext string:

var numcolors = colors.len;
var numchars = thetext.length;


Next, gradient( ) declares a series of variables that will track our progress along the color axis:

var rr = 0;
var gg = 0;
var bb = 0;
var lci = 0; // Lower color index
var hci = 0; // Higher color index


We are at long last ready to color the thetext characters. The coloring action is coordinated by a numchars-iteration for loop:

for (i = 0; i < numchars; ++i) { ... }

The i counter will double as a thetext character index. The loop first calls lowcolorindex( ) and hicolorindex( ) functions

lci = lowcolorindex(i, numchars, numcolors);
hci = hicolorindex(i, numchars, numcolors, lci);


that locate each thetext character with respect to the color axis; the i index and the numchars and numcolors lengths are passed to both functions, for which they are renamed x, y, and z, respectively. The lowcolorindex( ) return, lci, is also passed to hicolorindex( ) although it doesn't need to be, as we shall see. The lowcolorindex( ) and hicolorindex( ) functions are given below:

// x = Index of letter, y = Number of letters, z = Number of colors function lowcolorindex(x, y, z) { if (y == 1) return 0; else return Math.floor((x * (z - 1)) / (y - 1)); }

function hicolorindex(x, y, z, low) { if (low * (y - 1) == x * (z - 1)) return low; else if (y == 1) return 0; else return Math.floor((x * (z - 1)) / (y - 1) + 1); }

Let us normalize the length of the color axis to 2 as it contains two color-to-color transitions. Each character-to-character transition - each increase in x - moves us (z - 1) / (y - 1) along the color axis.

The i = 0 loop iteration is for the A character and signifies that no movement occurs along either axis. As i is 0, the lowcolorindex( ) Math.floor((x * (z - 1)) / (y - 1)) operation gives 0, which is returned and assigned to lci. As i and lci are 0, the hicolorindex( ) low * (y - 1) == x * (z - 1) if condition is true, and therefore low (0) is returned and assigned to hci.

The lowcolorindex( ) and hicolorindex( ) functions are set up to return the same value whenever a thetext character horizontally lines up with a colors color. The A character horizontally lines up with the colors.codes[0] color and its lci and hci indexes are both 0 in reflection of this alignment.

The B and C characters are respectively handled by the i = 1 and i = 2 loop iterations. For these characters the lci index is 0 and the hci index is 1 (⅓ and ⅔ are floor( )ed to 0, 1⅓ and 1⅔ are floor( )ed to 1), meaning that B and C are situated between the colors.codes[0] and colors.codes[1] colors.

The i = 3 iteration brings us to the D character, whose lci and hci indexes are both 1 - the x * (z - 1)) / (y - 1) calculation yields 1, the low * (y - 1) == x * (z - 1) if condition is true - per D's alignment with the colors.codes[1] color.

At this point you should be able to intuit that:
(E-F) The E and F characters have a 1 lci index and a 2 hci index as they are situated between the colors.codes[1] and colors.codes[2] colors.
(G) The G character's lci and hci indexes are both 2 per its alignment with the colors.codes[2] color.
You can work through the math if you want but it really isn't necessary.

Let's get back to the hicolorindex( ) low * (y - 1) == x * (z - 1) if condition for a moment. As intimated above, this condition flags those thetext characters that horizontally line up with colors colors. As it happens, the low term is superfluous. The x character will line up with a color whenever the x * (z - 1) product is a multiple of (y - 1) - e.g., if we were working with a 26-character thetext string and a 6-color colors object, then the x character would line up with a color for x = 0, 5, 10, 15, 20, and 25 - and we can therefore flag the alignments with an x * (z - 1) % (y - 1) == 0 condition. Wikipedia provides a detailed treatment of modulo operations here.

Is there a need for separate lowcolorindex( ) and hicolorindex( ) functions? Not in my book. We can recast the lowcolorindex( )/hicolorindex( ) functionality as:

if (1 <= (numchars - 1)) { // If we have at least one character-to-character transition lci = Math.floor(i * (numcolors - 1) / (numchars - 1)); hci = i * (numcolors - 1) % (numchars - 1) == 0 ? lci : Math.floor(i * (numcolors - 1) / (numchars - 1) + 1); }

The 1 <= (numchars - 1) if condition allows us to throw out the if (y == 1) return 0 statements in the original functions; recall that lci and hci are initialized to 0. The ?: conditional operator is documented here in the Mozilla JavaScript Reference.

We'll tackle the interpolate( ) function and wrap up our discourse on the Color Gradient Text script in the following entry.

Saturday, April 12, 2014
 
A Visit to the Visible Region, Part 2
Blog Entry #316

We continue today our deconstruction of the Java Goodies "Color Gradient Text" script. Having called the gradient( ) function and successfully gotten through a browser-checking gate, we next convert the thecolors string data into a structure that facilitates the creation of the script's gradient effect. We begin this process by feeding the thecolors string to a ColorList( ) constructor function. The ColorList( ) function constructs an Object object that is given a colors identifier.

var colors = new ColorList(thecolors);

We will use the first gradient( ) call's ff0000 ffffff 0000ff thecolors string in the discussion that follows.

The ColorList( ) function starts innocently enough by giving thecolors a new hexcodes identifier and declaring i and c variables.

function ColorList(hexcodes) { var i = 0; var c = 0;

Subsequently ColorList( ) defines a codes property as a three-element array, one element for each hexcodes color.

this.codes = new Array(Math.round(hexcodes.length / 7));

The ff0000 ffffff 0000ff string's length is 20, whose division by 7 gives 2.857142, which is Math.round( )ed up to 3.

Next we have a while loop that iterates over the hexcodes colors*

while (i < hexcodes.length) { ... }

and populates the codes boxes with child Object objects carrying the hexcodes data. (*Although the i < hexcodes.length condition seems to indicate that the loop iterates over the hexcodes characters, the loop actually leapfrogs from color to color via an i += 7; statement.) The while loop body comprises an if...else statement whose if clause checks if the i-to-i+6 hexcodes substring( ) - that would be ff0000 for the loop's first iteration - begins with a hexadecimal number.

if (isNaN(parseInt(hexcodes.substring(i, i + 6), 16))) ++i;

Go here for the current parseInt( ) page in the Mozilla JavaScript Reference; note that the parseInt( ) radix parameter is set to 16 (Mozilla exhorts authors to always set the radix value, even for a decimal operand). The parseInt( ) operation would return NaN ("Not-A-Number") - and therefore the isNaN( ) operation/if condition would return true - if we had prefaced ff0000 with a hash (#) mark, in which case the ++i; incrementation would push us to the starting f of ff0000 for the next iteration. (The if condition would also return true for a Hello World! hexcodes string, but let's assume that such strings are not on the menu, shall we?)

Given the hexcodes we're working with, however, the accompanying else clause is operative.

else { this.codes[c] = new ColorCode(hexcodes.substring(i, i + 6)); i += 7; ++c; }

The else clause sends ff0000 to a separate ColorCode( ) construction function.

function ColorCode(hexcode) { if (hexcode.length == 7) { this.r = parseInt(hexcode.substring(1, 3), 16); this.g = parseInt(hexcode.substring(3, 5), 16); this.b = parseInt(hexcode.substring(5, 7), 16); } else if (hexcode.length == 6) { this.r = parseInt(hexcode.substring(0, 2), 16); this.g = parseInt(hexcode.substring(2, 4), 16); this.b = parseInt(hexcode.substring(4, 6), 16); } else { this.r = this.g = this.b = 0; window.alert("Error: ColorCode constructor failed"); } if (isNaN(this.r) || isNaN(this.g) || isNaN(this.b)) window.alert("Error: ColorCode constructor failed"); }

The else if (hexcode.length == 6) clause is operative. The substring( ) calls grab the RR (ff), GG (00), and BB (00) parts of ff0000. The parseInt( ) calls respectively convert the ff/00/00 substring( )s to 255, 0, and 0**, which are respectively assigned to new r, g, and b properties of the ColorCode( ) object. (**A successful parseInt( ) operation returns a decimal integer regardless of its radix setting.)

The if (hexcode.length == 7) clause would not be operative if the hexcodes colors were prefaced with #s because
(a) the if clause in the ColorList( ) while loop would effectively subtract those #s and, more fundamentally,
(b) we don't pass more than six characters at a time to ColorCode( ) anyway.

It is left to the reader to verify that:
• The else clause that concludes the if...else if...else cascade would fire if the hexcodes string did not comprise 7n+6 characters (n=0,1,2,...), for example, f00 fff 00f would trigger it.
• The final if clause would fire if the (index-wise) second or fourth hexcode character were not a hexadecimal digit, e.g., ffg000 would trigger it.

Suffice it to say that if we check our work and make sure that the hexcodes string conforms to the original hexcode0 hexcode1 hexcode2 ... format - that's not too much to ask, is it? - then the else if (hexcode.length == 6) clause is the only one we need and the rest of it can be thrown out.

Returning to the else clause in the ColorList( ) while loop, the new ColorCode( ) object is assigned to colors.codes[0]. The loop's first iteration concludes by incrementing i to 7 and c to 1.

The loop's second iteration sends ffffff to the ColorCode( ) function and thereby produces a colors.codes[1] object with r, g, and b properties all set to 255. The loop's third iteration sends 0000ff to the ColorCode( ) function and produces a colors.codes[2] object whose r and g values are 0 and whose b value is 255. At the end of the third iteration i is pushed to 21; the loop stops when the i < hexcodes.length condition returns false at the beginning of (what would be) the fourth iteration.

For its part, the c index maxes out at 3; c's final value is assigned to a new len property at the end of the ColorList( ) function. (Unlike the Array object, the Object object does not have a length property.)

...while loop...
this.len = c; } // That's it for ColorList( )


In sum, the colors data structure is:

colors.codes[0].r = 255;
colors.codes[0].g = 0;
colors.codes[0].b = 0;
colors.codes[1].r = 255;
colors.codes[1].g = 255;
colors.codes[1].b = 255;
colors.codes[2].r = 0;
colors.codes[2].g = 0;
colors.codes[2].b = 255;
colors.len = 3;


Now, if all of the above seems like overkill to you, well, you would be right about that. The ColorList( )/ColorCode( ) functionality can be reproduced with:

var colors = thecolors.split(" "); var codes = new Array(colors.length); for (i = 0; i < colors.length; i++) { codes[i] = new Object( ); codes[i].r = parseInt(colors[i].substring(0, 2), 16); codes[i].g = parseInt(colors[i].substring(2, 4), 16); codes[i].b = parseInt(colors[i].substring(4, 6), 16); }

The thecolors string can be split into its constituent colors via the split( ) method of the String object. The codes array of Object objects can stand independently: there is no need to yoke it as a property to the colors object.

We will apply the r/g/b data to the thetext string in the following entry.

Sunday, April 06, 2014
 
A Visit to the Visible Region, Part 1
Blog Entry #315

For at least the next two entries we will discuss the Java Goodies "Color Gradient Text" script. Authored by "commoner" in the spring of 1998, the Color Gradient Text script applies a discontinuous spectrum of colors to a text string.

You can grab the Color Gradient Text script here or here. The script's effect looks best on a dark background - check out this page or this page.

gradient( ) overview

The Color Gradient Text script is set in motion by a gradient( ) function, whose calls have the following form:

gradient("This is the text string to be colored ...", "hexcode0 hexcode1 hexcode2 ...");

(1) The first gradient( ) argument is a text string of ASCII characters; if present, markup and character references are not acted on but are printed out literally.
(2) The second gradient( ) argument is a string of space-delimited hexadecimal RRGGBB color values not prefaced by hash (#) marks; other color value formats (color keywords, three-digit RGB values, rgb( ) functions) are not acceptable.

The script puts four gradient( ) calls in a script element in a centered div in the document body. Here's the first of those calls:

gradient("always wanted to be a genius?", "ff0000 ffffff 0000ff");

The ff0000, ffffff, and 0000ff values are for the colors red, white, and blue, respectively. The script sequentially runs the 29 characters of the "always wanted to be a genius?" string through a red-to-white-to-blue 'continuum':

• The starting lowercase a is pure red.

• The subsequent characters progressively become less red and more white culminating in a pure white t in the middle of the string.

• The subsequent characters progressively become less white and more blue culminating in a pure blue ? at the end of the string.

String lows and highs

If the text string has only one character, then that character will be painted the first RRGGBB color (hexcode0) as opposed to a blend of the RRGGBB colors. If the color string has only one value, then the text string will be given the default foreground color and not the hexcode0 color. Otherwise there really aren't any restrictions on the lengths of the text and color strings, but as a practical matter the number of colors must be less than the number of text characters in order to see a gradient effect.

Pre-color

The gradient( ) calls are preceded by some top-level code that is in fact unnecessary, but let's talk about it anyway, shall we? The script's executable JavaScript begins with some browser sniffing:

var browser = "unknown"; var version = 0;
if (navigator.userAgent.indexOf("Opera") >= 0) browser = "opera";
else if (navigator.userAgent.indexOf("obot") >= 0) browser = "robot";
else if (navigator.appName.indexOf("etscape") >= 0) browser = "netscape";
else if (navigator.appName.indexOf("icrosoft") >= 0) browser = "msie";
version = parseFloat(navigator.appVersion);
if (isNaN(version)) version = 0;
if ((browser == "msie") && (version == 2)) version = 3;


The browser and version flags were meant to weed out very early/unrecognized browsers whose JavaScript implementations were not up to snuff at the time. However, all of the script's features have specification-wise been in place from JavaScript 1.1 onward, and there is no need to hold onto any of the above statements. Be that as it may:

• Opera's navigator.userAgent string no longer contains "Opera" but its navigator.appName string does contain "etscape", i.e., Opera currently identifies as Netscape, as does Firefox and Safari.

• Go here for the current parseFloat( ) page and here for the current isNaN( ) page in the Mozilla JavaScript Reference.

• For all of the common browsers, the number at the beginning of the navigator.appVersion string generally does not correspond to the actual browser version: for example, on my computer Firefox 25.0.1's navigator.appVersion return is 5.0 (Macintosh). (I don't know if there are any browsers out there whose appVersion value does not begin with a number.) But again, this is code that we will be throwing out.

Subsequently the script creates an array of two-digit hexadecimal number strings running from "00" to "ff", inclusive:

var tohex = new Array(256); var hex = "0123456789abcdef"; var count = 0; for (x = 0; x < 16; x++) { for (y = 0; y < 16; y++) { tohex[count] = hex.charAt(x) + hex.charAt(y); count++; } }

The tohex array will later be used to build RRGGBB values for coloring the characters of the gradient( ) text string; however, we'll see that those characters can be colored more simply via a decimal rgb( ) function.

Past the sell-by date

Before we get to the script's functions, I want to briefly comment on the advertising banner code that appears at the beginning of the document body:

<body bgcolor="black" text="white" link="2070ff">
<!-- Begin Advertising Banner -->
<center>
<a href="http://adforce.imgis.com/?adlink|2|5823|621|1|IMGIS" target="_top">
<img src="http://adforce.imgis.com/?adserv|2|5823|621|1|IMGIS"
border="0" height="60" width="468" naturalsizeflag="0" align="bottom" alt="Another Wonderful HTML Goodies Advertiser"></a>
<p><hr width="60%"><p>
</center>
<!-- End Advertising Banner -->


• URLs should not contain unescaped pipe (|) characters.

• The nonstandard naturalsizeflag attribute is an HTML editor 'artifact' - read about it here. (Real coders do not use HTML editors, but you knew that, right?)

• You wouldn't expect the src="http://adforce.imgis.com/?adserv|2|5823|621|1|IMGIS" image to still be available, and it isn't. Modern browsers apply the body's 2070ff link color to the image alt text.

gradient( ) intro

We're ready to get the gradient( ) action under way. The text string and color string arguments are given the identifiers thetext and thecolors, respectively:

function gradient(thetext, thecolors) { ... }

The browser first goes through the following gate:

if (((browser == "netscape") || (browser == "msie") || (browser == "opera")) && (version >= 3.0)) { ... } else document.write(thetext);

The gradient( ) function then calls on ColorList( ) and ColorCode( ) functions to separate and decimalize the RR, GG, and BB parts of the thecolors colors - this code deserves its own entry so we'll deal with it next time.


Powered by Blogger

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