/** NovaLoka's BX_BLENDER program - August 2007 - copyright Frans Lelieveld */

/** final String[][]  
    Array of supported color names and their hexadecimal values. */
var bx_colorNamesMap = [ ["black","#000000"], ["silver","#c0c0c0"], ["gray","#808080"], 
      ["white","#ffffff"], ["maroon","#800000"], ["red","#ff0000"], ["purple","#800080"], 
      ["fuchsia","#ff00ff"], ["magenta","#ff00ff"], ["green","#008000"], ["lime","#00ff00"], 
      ["olive","#808000"], ["yellow","#ffff00"], ["navy","#000080"], ["blue","#0000ff"], 
      ["teal","#008080"], ["aqua","#00ffff"], ["cyan","#00ffff"], ["orange","#ffa500"], 
      ["pink","#ffc0cb"], ["brown","#a52a2a"], ["violet","#ee82ee"], ["gold","#ffd700"] ];
      
/** final int
    The default delay (between style values) is an esthetic 3 seconds. */
var bx_DELAY_MS = 3000;

/** final int
    The minimal frame delay is 1000ms/80Hz. */
var bx_FRAME_MS = 13;

/** final int
    Default single tone clarity for use with inverse alternative colors for gray. */
var bx_ALT_CLARITY = 60;

/** final int
    Default strength of parent blends at color merges. */
var bx_MERGE_LEVEL = 60;


/** public
 *
 * Easy method to apply blending to a style property of an HTML element.
 * The valueList here is a String that contains values separated by spaces
 * or by optional quotes, as in: 'Times New Roman' Arial "Courier New"
 * Applies the default delay and resolution as specified for bx_apply().
 *
 * @param  REQ  String          elemId      id of the HTML Element
 * @param  REQ  String          styleProp   a style property name
 * @param  REQ  String          valueList   space separated list of values
 * @param  OPT  boolean | int   loops       autoloop on/off or set a number of repeats
 *
 * @return  bx_Blender   the newly created blending object, or null if error
 */
function bx_easy(elemId, styleProp, valueList, loops)
{
  // Split list on quotes and/or whitespace - PP chokes on escaped quotes!
  var arr = valueList.split(/\s*['"]\s*|\s+/);     // "' // or whitespace
  
  return bx_apply(elemId, styleProp, arr, bx_DELAY_MS, true, loops);
}

/** public
 *
 * Apply blending of some style property to an HTML element on a webpage.
 * Make sure the element has been loaded before calling this function. 
 * Colors formats can be combined, as in [ "red", "#00ff00", "rgb(0,0,255)" ], but 
 * this method expects numeric values to be expressed in the same units, eg. all 'px'.
 *
 * @param  REQ  String            elemId     id of HTML Element to apply blending to
 * @param  REQ  String            styleProp  name of style property to blend
 * @param  REQ  int[] | String[]  values     (array of start to) end value for style prop,
 *              int | String                 any numbers are converted to String,
 *                                           if the unit is missing the default is 'px'
 * @param  OPT  int               delay      time in millisecs to go from value to value,
 *                                           default: bx_DELAY_MS
 * @param  OPT  boolean | float%  resolut    resolution min/max or set percentage,
 *                                           default: 100% = maximal resolution
 * @param  OPT  boolean | int     loops      autoloop on/off or set a number of repeats,
 *                                           default: true = autoloop on
 * @param  OPT  int               release    millisecs to interrupt blending at first value
 *
 * @return  bx_Blender   the new blending object, or 
 *          null         if none could be created
 */
function bx_apply(elemId, styleProp, values, delay, resolut, loops, release)
{
  // Get HTML element
  var elem = document.getElementById(elemId);
  if(!elem)
    return null;
  
  // Check and rename CSS style-property to JavaScript styleProperty
  styleProp = bx_styleName(styleProp);
  if(!styleProp)
    return null;
  
  // If values has only 1 value it is an end value..
  // ..then get the elem style value as start value
  if(typeof values != 'object')
    values = [ values ];
  if(values.length == 1)
  {
    if(!elem.style[styleProp])      // no start value - no blend possible
      return null;
    values = [ elem.style[styleProp], values[0] ];
  }
  
  var arr = [];                    // temp array to sift out values
  
  if(styleProp.search(/color/i) != -1)       // format color values
  {
    var isColor = true;
    
    for(var i = 0, j = 0; i < values.length; i++)
    {
      var col = bx_colorFormat(values[i]);   // "#RRGGBB" | false
      if(col)
        arr[j++] = col;
    }
  } 
  else                                       // other values - eg. numeric
  {
    var unit, isNumeric;
    
    for(var i = 0, j = 0; i < values.length; i++)
    {
      var val = values[i] = values[i].toString();
      
      if(val.indexOf(' ') != -1)             // space inside a value?
        if(val.search(/\S/) == -1)           // only space?
          continue;
        else if(val.search(/\S \S/) != -1)   // value is compound?
        {
          isNumeric = false;
          break;
        }
        
      var num = parseInt(val);
      if(isNaN(num))
      {
        isNumeric = false;
        break;
      }
      isNumeric = true;
      arr[j++] = num;
      
      if(!unit)
      {
        var mat = val.match(/[a-zA-Z]+/);
        if(mat)
          unit = mat[0];
      }
    }
    
    if(isNumeric && !unit)
      unit = 'px';                   // the default numeric unit is 'px' = in pixels
  }
  
  if(isColor || isNumeric)
    values = arr;
  if(values.length < 2)
    return null;
  
  // Check delay
  if(typeof delay != 'number' || delay <= 0)
    delay = bx_DELAY_MS;             // default delay
  else if(delay < bx_FRAME_MS)
    delay = bx_FRAME_MS;             // reset to minimal delay
  // Change boolean resolution to int
  if(typeof resolut == 'undefined')
    resolut = 100;                   // maximal smooth
  else if(resolut === false)
    resolut = 0;                     // blink
  else if(resolut === true)
    resolut = 100;                   // maximal smooth
  else if(resolut < 0 || resolut > 100)
    resolut = resolut < 0 ? 0 : 100; // in proper range
  // Change boolean autoloop to int
  if(typeof loops == 'undefined')
    loops = Number.MAX_VALUE;        // arbitrary loop
  else if(loops === false)
    loops = 1;                       // 0 stays at start value
  else if(loops === true)
    loops = Number.MAX_VALUE;        // infinite loop
  else if(loops < 0)
    loops = -loops;                  // positive number
  if(typeof release != 'number' || release < 0)
    release = 0;                     // milliseconds interruption
  
  // Calculate blends for different values
  var blends, len;
  if(isColor)
  {
    blends = bx_blendColorArray(values, delay, resolut, loops > 1);
    len = values.length + (loops > 1 ? 1 : 0);
  }
  else if(isNumeric)
  {
    blends = bx_blendNumericArray(values, unit, delay, resolut, loops > 1);
    len = values.length + (loops > 1 ? 1 : 0);
  }
  else
  {
    blends = values;                 // needs no inbetween values
    len = values.length;
  }
  
  var frameMS = Math.round(delay * len / blends.length);
  var blender = new bx_Blender(elem, styleProp, blends, frameMS, loops, release);
  
  blender.start();
  return blender;
}

/**
 * Rename a CSS style property to its JavaScript style equivalent. 
 *
 * @param  REQ  String  prop    CSS (or JS) style property
 *
 * @return  String    the JS style property
 */
function bx_styleName(prop)
{
  if(!prop)
    return null;
  
  while(prop.indexOf('-') != -1)
  {
    var i = prop.indexOf('-');
    prop = prop.substring(0, i) + 
           prop.charAt(i + 1).toUpperCase() +
           prop.substring(i + 2);
  }
  // PP add real style name check?
  return prop;
}

/**
 * Parse a given color to the hexadecimal standard format "#RRGGBB". 
 *
 * @param  REQ  String  colorValue   color as either: "#RRGGBB" | "#RGB" | "rgb(N,N,N)" |
 *                                                    "rgb(N%, N%, N%)" | "RRGGBB" | "name"
 *
 * @return  String    color in hexadecimal format, or
 *          boolean   false   if the colorValue cannot be parsed
 */
function bx_colorFormat(colorValue)
{
  if(!colorValue || typeof colorValue != 'string')
    return false;             // no argument
  
  var col = colorValue;
  
  if(col.charAt(0) == '#')
  {
    if(col.length == 7)       // eg. "#FF00ff" - assume it's OK
      return col;
    if(col.length == 4)       // eg. "#F00"
      return '#' + col.charAt(1) + col.charAt(1) +
                   col.charAt(2) + col.charAt(2) +
                   col.charAt(3) + col.charAt(3);
  }
  else
  {
    if(col.search(/rgb/i) != -1)               // eg. "rgb(255, 100%, 0)"
    {
      var rgb = col.match(/\d+%?/g);           // find 3 color values

      col = '#';
      for(var i = 0; i < rgb.length; i++)
      {
        var s = rgb[i];
        if(s.indexOf("%") != -1)               // percentage to numeral value
          s = Math.round(parseInt(s) * 2.55);
          
        var h = parseInt(s).toString(16);      // numeral to hexadecimal value
        col += (h.length == 1 ? '0' : '') + h;
      }
      if(col.length == 7)                      // final check
        return col;
    }
    else if(col.search(/\d{6}/) == 0)          // eg. "000000"
      return '#' + col.substring(0, 6);
    else                                       // eg. "lime"
    {
      col = col.toLowerCase();
      for(var i = 0; i < bx_colorNamesMap.length; i++)
        if(col == bx_colorNamesMap[i][0])
          return bx_colorNamesMap[i][1];       // hexadecimal code, eg. "#00ff00"
    }
  }
  return false;                                // eg. "inherit", "transparent" - not OK
}

/**
 * Create an array of blended color values, as specified by the parameters.
 *
 * @param  REQ  String[]   values    hexadecimal color array of start to end value
 * @param  REQ  int        delay     time in millisecs to go from value to value
 * @param  REQ  float%     resolut   resolution as a percentage
 * @param  REQ  boolean    loop      include loop back from end to start
 *
 * @return  String[]   array of hexadecimal colors, blended
 */
function bx_blendColorArray(values, delay, resolut, loop)
{
  var frames = Math.floor(delay/bx_FRAME_MS * resolut/100);
  var blends = [ values[0] ];
  
  for(var i = 1; i <= values.length; i++)
  {
    // Handle start value
    var startRGB = endRGB ? endRGB : bx_toRGB(values[i-1]);
    
    // Handle end value
    if(i == values.length) 
    {
      if(!loop)
        break;

      if(i == 2)       // loops with two values are like toggles
      {
        for(var j = blends.length-2; j > 0; j--)
          blends[blends.length] = blends[j];
        break;
      }
        
      var endRGB = bx_toRGB(values[0]);
      var limit = frames - 1;
    }
    else
    {
      var endRGB = bx_toRGB(values[i]);
      var limit = frames;
    }

    // Calculate color blends inbetween
    for(var j = 1; j <= limit; j++)
    {
      var w = j/frames;
      var rgb = [ bx_weighNumber(startRGB[0], endRGB[0], w),
                  bx_weighNumber(startRGB[1], endRGB[1], w),
                  bx_weighNumber(startRGB[2], endRGB[2], w) ];
      blends[blends.length] = bx_toHex(rgb);
    }
  }
  
  return blends;
}

/**
 * Convert a hexadecimal color to an RGB array.
 *
 * @param  REQ  String  hex   color in "#RRGGBB" format
 *
 * @return  int[]   array of length 3 with RGB colors as numbers
 */
function bx_toRGB(hex)
{
  return [ parseInt(hex.substring(1, 3), 16),
           parseInt(hex.substring(3, 5), 16),
           parseInt(hex.substring(5), 16) ];
}

/**
 * Convert an RGB array to a hexadecimal color.
 *
 * @param  REQ  int[]  rgb   array of length 3 with RGB colors as numbers
 *
 * @return  String   color in "#RRGGBB" format
 */
function bx_toHex(rgb)
{
  var col = '#';
  for(var i = 0; i < 3; i++)
  {
    var h = rgb[i].toString(16);
    col += (h.length == 1 ? '0' : '') + h;
  }
  return col;
}

/**
 * Create an array of blended numeric style values, as specified by the parameters.
 *
 * @param  REQ  int[]      values    integer array of start to end value
 * @param  REQ  String     unit      unit to append to the values
 * @param  REQ  int        delay     time in millisecs to go from value to value
 * @param  REQ  float%     resolut   resolution as a percentage
 * @param  REQ  boolean    loop      include loop back from end to start
 *
 * @return  String[]   array of style values, with a certain 'blending'
 */
function bx_blendNumericArray(values, unit, delay, resolut, loop)
{
  var maxFrames = Math.floor(delay/bx_FRAME_MS * resolut/100);
  var maxDistance = 0;
  for(var i = 0; i < values.length-1; i++)
  {
    var dist = Math.abs(values[i+1] - values[i]);
    if(dist > maxDistance)
      maxDistance = dist;
  }
  var frames = Math.min(maxFrames, maxDistance);
  if(frames < 2)
    return values;
  
  var blends = [ values[0] + unit ];
  
  for(var i = 1; i <= values.length; i++)
  {
    // Handle start value
    var startValue = endValue ? endValue : values[i-1];
    
    // Handle end value
    if(i == values.length)
    {
      if(!loop)
        break;
      
      if(i == 2)       // loops with two values are like toggles
      {
        for(var j = blends.length-2; j > 0; j--)
          blends[blends.length] = blends[j];
        break;
      }
      
      var endValue = values[0];
      var limit = frames - 1;
    }
    else
    {
      var endValue = values[i];
      var limit = frames;
    }
    
    // Calculate numeric blends inbetween
    for(var j = 1; j <= limit; j++)
    {
      var w = j/frames;
      var num = bx_weighNumber(startValue, endValue, w);
      blends[blends.length] = num + unit;
    }
  }
  
  return blends;
}

/**
 * Calculate a number inbetween two others, where a weight specifies its position.
 *
 * @param  REQ  int    startNum   the first number (at weight 0)
 * @param  REQ  int    endNum     the last number (at weight 1)
 * @param  REQ  float  weight     proportion (0<=P<=1) of startNum against endNum
 *
 * @return  int   an integer number positioned inbetween 
 */
function bx_weighNumber(startNum, endNum, weight)
{
  return Math.round(startNum + (endNum - startNum) * weight);
}

/**
 * Invert a hexadecimal color array. Optionally avoiding the gray area.
 *
 * @param  REQ  String[]  cols      array of "#RRGGBB" color values
 * @param  OPT  String    altGray   alternative "#RRGGBB" color to avoid grays
 * @param  OPT  int       clarity   tone clarity for altGray, range 0 - 256,
 *                                  default: bx_ALT_CLARITY
 *
 * @return  String[]   a new inverse color array
 */
function bx_colorsInverse(cols, altGray, clarity)
{
  var invs = new Array(cols.length);
  var altRGB = altGray ? bx_toRGB(altGray) : false;
  
  if(typeof clarity != 'number')
    clarity = bx_ALT_CLARITY;    // default single tone clarity value
  
  for(var i = 0; i < cols.length; i++)
    invs[i] = bx_colorInverse(cols[i], altRGB, clarity);
  
  return invs;
}

/**
 * Invert a hexadecimal color. Optionally avoiding the gray area.
 *
 * @param  REQ  String   col       "#RRGGBB" color value
 * @param  REQ  int[] |  altRGB    alternative RGB array to avoid grays,
 *              boolean  false     if grays are to remain
 * @param  REQ  int      clarity   single tone clarity for altRGB,
 *
 * @return  String   the inverse color
 */
function bx_colorInverse(col, altRGB, clarity)
{
  var rgb = bx_toRGB(col);
  
  for(var i = 0; i < 3; i++)
    rgb[i] = 255 - rgb[i];

  if(altRGB)
  {
    var grayTone = Math.abs(128 - rgb[0]) + 
                   Math.abs(128 - rgb[1]) +
                   Math.abs(128 - rgb[2]);

    if(grayTone < 3*clarity)   // if grayish, bend color tones towards altRGB color
    {
      for(var i = 0; i < 3; i++)
      {
        grayTone = Math.abs(128 - rgb[i]);
        
        if(grayTone < clarity)
          rgb[i] = bx_weighNumber(altRGB[i], rgb[i], grayTone/clarity);
      }
    }
  }
  
  return bx_toHex(rgb);
}


/** constructor
 *
 * Construct a blender for an HTML Element and add it to the window blenders.
 *
 * @param  REQ  String     elem       HTML Element to apply blending to
 * @param  REQ  String     prop       name of style property to blend
 * @param  REQ  String[]   blends     array of blended values for style prop
 * @param  REQ  int        frameMS    delay in millisecs 'between frames'
 * @param  REQ  int        loops      number of repeats
 * @param  REQ  int        release    millisecs to interrupt blending at first value
 *
 * @return  void
 */
function bx_Blender(elem, prop, blends, frameMS, loops, release)
{
  this.elem = elem;           // elem with id
  this.prop = prop;
  this.blends = blends;
  this.delay = frameMS;       // interval delay
  this.loops = loops;         // number of loops
  this.release = release;     // stop for release ms. at zeroth index of values
  
  // Remove and clear former object + children..
  // ..if it has the same "elem + prop" as this
  bx_recall(window.bx_Blenders, this.elem, this.prop, true);

  this.children = [];
  this.bx = window.bx_Blenders.length;    // this object index
  
  // Make this blender part of the blenders in the window
  window.bx_Blenders[this.bx] = this;
}

/**
 * Starts off this blender.
 *
 * @return  void
 */
bx_Blender.prototype.start = function() 
{
  this.counter = 0;          // loop counter
  this.index = 0;
  this.elem.style[this.prop] = this.blends[this.index];
  
  if(this.release)
    this.timer = setTimeout('bx_Blenders[' + this.bx + '].bx_flow();', this.release);
  else
    this.timer = setInterval('bx_Blenders[' + this.bx + '].bx_flow();', this.delay); 
};

/** 
 * Stops this blender.
 *
 * @return  void
 */
bx_Blender.prototype.stop = function() 
{
  if(this.release)
    clearTimeout(this.timer);
  else
    clearInterval(this.timer);
  this.timer = 0;
  
  this.elem.style[this.prop] = "";       // clear for other DHTML
}

/**
 * Initiate the next blending with the next index.
 *
 * @return  void
 */
bx_Blender.prototype.bx_flow = function() 
{
  // Set index or quit
  if(++this.index >= this.blends.length)
  {
    this.index = 0;
    
    if(++this.counter >= this.loops)     // quit loop
    {
      if(this.loops > 1)
        this.bx_style();                 // final style
      
      this.stop();
      return;
    }
  }
  this.bx_style();                       // new style
  
  if(this.release)
    this.timer = setTimeout('bx_Blenders[' + this.bx + '].bx_flow();', 
                            this.index == 0 ? this.release : this.delay);
}

/** 
 * Put the blending styles on the webpage, for blender and children.
 *
 * @return  void
 */
bx_Blender.prototype.bx_style = function()
{
  this.elem.style[this.prop] = this.blends[this.index];   // this object's style
        
  if(this.children)                                       // handle child styles
  {
    for(var i = 0; i < this.children.length; i++)
    {
      var child = this.children[i];
      
      var j = this.index;
      if(child.shift)
        j = (j + child.shift) % this.blends.length;       // shift index
      
      if(child.blends)
        child.elem.style[child.prop] = child.blends[j];
      else
        child.elem.style[child.prop] = this.blends[j];
    }
  }
}

/**
 * Add a child to this blender, or reset an existing child.
 * An undefined optValue is a 'plain' (re)setting for an (existing) child.
 *
 * @param  REQ  Object    HTMLElem   HTML-Element on the page
 * @param  REQ  String    JSStyle    style property in JavaScript format
 * @param  OPT  String[]  optValue   array of style values instead of blends, or
 *              | int                shift of the index into blends
 *
 * @return  object    the blender child, or null if it could not be set
 */
bx_Blender.prototype.setChild = function(HTMLElem, JSStyle, optValue)
{
  var type = 0;                    // default type: 'plain' reset
  if(typeof optValue == 'object')
  {
    type = 1;                      // type: String[]
    // Check if child and parent can work as a team
    if(optValue.length != this.blends.length)
      return null;
  }
  else if(typeof optValue == 'number')
    type = 2;                      // type: int
  
  // Check if child was added before
  var child = bx_recall(this.children, HTMLElem, JSStyle);
  
  if(child)
    switch(type)
    {
      case 1: 
        child.blends = optValue;   // brand new blends
        break;
      case 2:
        child.shift = optValue;    // reset shift
        break;
      default:
        child.blends = null;       // total reset
        child.shift = 0;
    }
  else                                            // set properties for new child
    child = this.children[this.children.length] = {
        parent: this,
        elem: HTMLElem,
        prop: JSStyle,
        blends: (type == 1 ? optValue : null),    // array of values
        shift: (type == 2 ? optValue : 0)         // shift of index
                                                  };
                                                  
  var index = child.shift ? (child.shift % this.blends.length) : 0;
  child.firstBlend = child.blends ? child.blends[index] : this.blends[index];
  
  // Quickly adapt to first color - PP could be made a smooth fast blend?!
  child.elem.style[child.prop] = child.firstBlend;

  return child;
}


/** public
 *
 * Relate child Element(s)+Prop to a parent Element+Prop, already applied as a blender.
 * When the properties differ, a childs element can be its own parent.
 * This method has 4 options: plain (default), inverse, shift and merge.
 * Existing child properties are adjusted, only 'plain' clears such properties.
 *
 * @param  REQ  String   parentElemId  id of HTML Element applied as blender
 * @param  REQ  String   parentProp    its applied style property
 * @param  REQ  String childIds        id of single HTML Element to be newly spawned
 *              | String[]             or an array of ids, or ids as ['prefix']
 *                                     (tries to postfix numbers to create valid ids)
 * @param  OPT  String   childProp     style property to be newly spawned,
 *                                     default: same as parentProp
 * @param  OPT  String   optName       'inverse' = use inverse of blend values
 *                                     'shift' = shift in blends range
 *                                     'merge' = merge blends with standard value
 *                                  QQ NEW! 'contrast' = strengthen or weaken blend values
 * @param  OPT  String   optValue      @inverse - optional color to avoid grays
 *              String | float%        @shift - value into blends or a shift percentage
 *              String                 @merge - standard style value of child
 *                                  QQ NEW! '@contrast - factor as a percentage: -100 to 100
 * @param  OPT  int      optLevel      @inverse - tone clarity for optValue, range 0 - 256
 *                                     @merge - strength of parent blends, 
 *                                              usual range: 1 to 99, inverses: -1 to -99
 *
 * @return  object[]   always an array of proper blender's childs,
 *                     | null if it could none be set
 */
function bx_spawn(parentElemId, parentProp, childIds, childProp, 
                  optName, optValue, optLevel)
{
  // Prepare args for bx_spawn_child()
  var parentElem = document.getElementById(parentElemId);
  parentProp = bx_styleName(parentProp);
  
  if(!parentElem || !parentProp || !childIds.length)
    return null;

  var blender = bx_recall(window.bx_Blenders, parentElem, parentProp);
  if(!blender)
    return null;
  
  if(childProp)
    childProp = bx_styleName(childProp);
  if(!childProp)
    childProp = parentProp;
  
  var isColor = (childProp.search(/color/i) != -1);
  if(isColor && typeof optValue != 'number')
    optValue = bx_colorFormat(optValue);        // "#RRGGBB" format, false if undefined
  
  var children = [];            // return value;
  
  // Handle single child id
  if(typeof childIds == 'string') 
  {
    var childElem = document.getElementById(childIds);

    if(!childElem || childElem == parentElem)   // user error? try to use as prefix!
      return bx_spawn(parentElemId, parentProp, [ childIds ], childProp, 
                      optName, optValue, optLevel);
    else
      children = [ bx_spawn_child(blender, childElem, childProp, isColor, 
                                  optName, optValue, optLevel) ];
  }
  else
  {
    // Handle array of child ids - child cannot be its own parent
    id_loop:
    for(var i = 0; i < childIds.length; i++)
    {
      var id = childIds[i];
      var childElem = document.getElementById(id);
      var child;
      
      // Try to use childIds[i] as a prefix by adding 4 numbers
      if(childIds.length == 1 || !childElem || childElem == parentElem)
      {
        var down = 4;                             // abort after 4*2 failures
        for(var j = 0; j < 1000; j++)
        {
          childElem = document.getElementById(id + j);
          if(!childElem)
            childElem = document.getElementById(id + '_' + j);
          if(!childElem)
            if(--down <= 0)
              continue id_loop;
            else
              continue;
          // Next child is found
          child = bx_spawn_child(blender, childElem, childProp, isColor, 
                                 optName, optValue, optLevel);
          if(child)
            children[children.length] = child;
        }
      }
      if(children.length == 0 && childElem && childElem != parentElem)
      {
        child = bx_spawn_child(blender, childElem, childProp, isColor, 
                               optName, optValue, optLevel);
        if(child)
          children[children.length] = child;
      }
    }
  }
  
  if(children.length == 0)
    return null;
  
  // Also return  if(children.length == 1)  as [child]
  return children;
}
  
  
/** 
 * Called by bx_spawn() after it prepared the user arguments to spawn a child.
 *
 * @param  REQ  String   blender       the blender of the parent element
 * @param  REQ  Object   childElem     HTML Element to be attached to the blender
 * @param  REQ  String   childProp     style property to be newly spawned
 * @param  REQ  boolean  isColor       flags if childProp is a color property
 * @param  OPT  String   optName       'inverse' = use inverse of blend values
 *                                     'shift' = shift in blends range
 *                                     'merge' = merge blends with standard value
 * @param  OPT  String   optValue      @inverse - optional color to avoid grays
 *              String | float%        @shift - value into blends or a shift percentage
 *              String                 @merge - standard style value of child
 * @param  OPT  int      optLevel      @inverse - tone clarity for optValue, range 0 - 256
 *                                     @merge - strength of parent blends, 
 *                                              usual range: 1 to 99, inverses: -1 to -99
 *
 * @return  object   the blender's child, or null if it could not be set
 *
 * @see #bx_spawn
 */
function bx_spawn_child(blender, childElem, childProp, isColor, 
                        optName, optValue, optLevel)
{
  // Check if child was added before
  if(optName)
    var child = bx_recall(blender.children, childElem, childProp);
  
  if(optName == 'merge')
  {
    if(typeof optLevel != 'number')
      optLevel = bx_MERGE_LEVEL;
    
    if(optLevel == 0)
    {
      if(child)
        bx_recall(blender.children, HTMLElem, JSStyle, true);  // remove child
      
      childElem.style[childProp] = optValue;                   // fixed value
      return null;                                             // no such child (anymore)
    }
    else if(optLevel == 100)
      optName = 'plain';
    else
    {
      if(!optValue)             // if undefined, try to find the value
      {
        var value = childElem.style[childProp];
        if(value)
          optValue = value;
        else if(isColor)
          optValue = (childProp.indexOf('background') != -1) ? "#ffffff" : "#000000";
        else
          return null;
      }
      
      var oldBlends = (child && child.blends) ? child.blends : blender.blends;
      var blends = [];
      
      // Create array of inverse style values
      if(isColor)
        blends = bx_colorsMerge(oldBlends, optValue, optLevel);
      else
        blends = bx_numbersMerge(oldBlends, optValue, optLevel);
      
      if(!blends)
        return null;
      
      return blender.setChild(childElem, childProp, blends);
    }
  }
  if(optName == 'inverse')
  {
    var oldBlends = (child && child.blends) ? child.blends : blender.blends;
    var blends = [];
    
    // Create array of inverse style values
    if(isColor)
      blends = bx_colorsInverse(oldBlends, optValue, optLevel);
    else
      for(var i = oldBlends.length-1, j = 0; i > -1; i--)
        blends[j++] = oldBlends[i];

    return blender.setChild(childElem, childProp, blends);
  }
  else if(optName == 'shift')
  {
    var index = -1;      // shift starting from index into blends
    
    if(typeof optValue == 'string')           // string optValue is an entry in blends
    {
      for(var i = 0; i < blender.blends.length; i++)
        if(blender.blends[i] == optValue)
          index = i;
    }
    else if(typeof optValue == 'number')     // numeric optValue is percentage
    {
      if(optValue >= 0 && optValue <= 100)
      {
        index = Math.round((blender.blends.length-1) * optValue/100);
        if(child && child.shift)
          index += child.shift;              // the shift is an adjust
      }
    }
    
    if(index == -1)                          // optValue lacking..
      if(child && child.shift)
        return null;                         // ..maintain shift
      else
        index = Math.floor(blender.blends.length / 2);    // ..halfway index
    
    return blender.setChild(childElem, childProp, index);
  }

  // The 'plain' option, where the child assumes the same values as the parent
  return blender.setChild(childElem, childProp);
}

/**
 * Merge a color value with an array of blended colors.
 * Does not change the blends array.
 * The level can be set in the range of -100 and 100, where:
 *           100 is the same as the blends colors
 *           from 1 to 99 the color merges with the blends colors
 *           at 0 the single color stays unchanged
 *           from -1 to -99 the color merges with the inverses of blends
 *           -100 equals the inverse colors of the blends
 * Outside of this range the merges tend to fade into black or white.
 *
 * @param  REQ  String[]  blends   array of hexadecimal colors
 * @param  REQ  String    color    single "#RRGGBB" color to merge with
 * @param  REQ  int       level    percentage of blends strength against color
 *
 * @return  String[]  a new merged array of hexadecimal colors,
 *                    null if error
 */
function bx_colorsMerge(blends, color, level)
{
  if(!color)
    return null;
  
  var merges = [];
  
  // Copy the array as a singular or an inverse or a regular
  if(level == 0)
    for(var i = 0; i < blends.length; i++)
      merges[i] = color;
  else if(level < 0)
    var merges = bx_colorsInverse(blends);
  else
    for(var i = 0; i < blends.length; i++)
      merges[i] = blends[i];
  
  level = Math.abs(level);    // blends strength as a percentage
  if(level == 0 || level == 100)
    return merges;            // no merge
  
  var col = bx_toRGB(color);
  var w = level/100;          // blends strength as a proportion
  
  for(var i = 0; i < merges.length; i++)       // merge values
  {
    var rgb = bx_toRGB(merges[i]);
    
    for(var j = 0; j < 3; j++)
    {
      rgb[j] = bx_weighNumber(col[j], rgb[j], w);
      rgb[j] = Math.min(255, Math.max(0, rgb[j]));
    }
    
    merges[i] = bx_toHex(rgb);
  }

  return merges;
}

/**
 * Merge a style value with an array of blended style values.
 * Does not change the blends array.
 *
 * @param  REQ  String[]  blends   array of style values
 * @param  REQ  String    value    single style value to merge with
 * @param  REQ  int       level    percentage of blends strength against value
 *
 * @return  String[]  a new merged array of style values,
 *                    null if error occurred
 */
function bx_numbersMerge(blends, value, level)
{
  // Check value and its unit
  var number = parseInt(value);
  if(isNaN(number))
    return null;
  var unit = blends[0].match(/[a-zA-Z]+/)[0];
  if(number != 0)
  {
    unit2 = value.match(/[a-zA-Z]+/)[0];
    if(unit != unit2)
      return null;
  }
    
  var merges = [];
  
  // Copy the array as a singular or an inverse or a regular
  if(level == 0)
    for(var i = 0; i < blends.length; i++)
      merges[i] = number + unit;
  else if(level < 0)
    for(var i = blends.length-1, j = 0; i > -1; i--)    // a kind of inverse
      merges[j++] = blends[i];
  else
    for(var i = 0; i < blends.length; i++)
      merges[i] = blends[i];
    
  level = Math.abs(level);
  if(level == 0 || level == 100)
    return merges;
  
  var w = level/100;          // blends strength as a proportion
  
  // Merge number values
  for(var i = 0; i < merges.length; i++)
  {
    var num = parseInt(merges[i]);
    num = bx_weighNumber(number, num, w);
    merges[i] = num + unit;
  }

  return merges;
}

/**
 * Get or remove an entry from an array of Objects that have 'elem' + 'prop' properties.
 * Eg. a bx_Blender from the window list, or a child from a blender.
 *
 * @param  REQ  Object[]  ObjArr       array of Objects with elem and prop properties
 * @param  REQ  Object    HTMLElem     HTML-Element on the page
 * @param  REQ  String    JSStyle      style property in JavaScript format
 * @param  OPT  boolean   remove       remove the Blender from the window list
 *
 * @return  Object   if such an entry could be recalled,
 *                   null if there is none
 */
function bx_recall(ObjArr, HTMLElem, JSStyle, remove)
{
  for(var i = 0; i < ObjArr.length; i++)
  {
    var obj = ObjArr[i];
    if(obj.elem == HTMLElem && obj.prop == JSStyle)
    {
      if(remove)
      {
        if(obj.stop)      // stop blenders
          obj.stop();
        ObjArr[i] = null;
      }
      return obj;         // recall
    }
  }
  return null;            // no recall
}

/** public
 *
 * Stop or restart all Blenders.
 *
 * @param  OPT  boolean   restart   false stops all Blenders in the window list, 
 *                                  true starts them up again
 * @return  void
 */
function bx_control(restart)
{
  var size = window.bx_Blenders.length;
  
  for(var i = 0; i < size; i++)
  {
    var obj = window.bx_Blenders[i];
    if(obj != null)
      if(restart)
        obj.start();
      else
        obj.stop();
  }
}

/** public
 *
 * Relate a child Element+Prop to a parent Element+Prop, already applied as a blender.
 *
 * @param  REQ  String    parentElemId  id of HTML Element applied as blender
 * @param  REQ  String    parentProp    its applied style property
 * @param  REQ  String    parentPos     position given by the first such style value, or
 *              | float%                a percentage of progress in the blends loop
 * @param  REQ  String    childElemId   id of the dependent HTML Element
 * @param  OPT  String    childProp     style property to be newly spawned,
 *                                      default: same as parentProp
 * @param  OPT  function  childFunct    
 *
 * @return  object    the blender's child, or null if it could not be set
 */
function bx_functor(parentElemId, parentProp, parentPos,
                    childElemId, childProp, childFunct)
{
  // QQ
  return null;
}


/** Object[]  bx_Blender[]   internal list of all bx_Blenders  */
var bx_Blenders = [];

/** public  Object  bx   holds the public interface for this program  */
var bx = { 
           easy: bx_easy,
           apply: bx_apply,
           spawn: bx_spawn,
           control: bx_control
         };


/** public
 *
 * Randomly select a number of entries from a continuous array.
 * The optional fixed first entry is not included in this number (it adds one).
 * Each 'random cycle' will contain every entry of the array,
 * this cycle algorithm guarantees a minimum number of duplicates,
 * and also that no two adjacent entries will be equal (singles - no duos).
 * Arrays of length 2 are different, here I let singles and duos alternate.
 * Although noCycle is truly random, it allows for unesthetic duplicates.
 *
 * @param  REQ  *[]      array        any array where all entries are defined
 * @param  REQ  int      number       number of random values, or
 *              | int[]               a random number in the range [start, end]
 * @param  OPT  boolean  noCycle      set to true if duplicates are no problem
 * @param  OPT  int      firstIndex   index of a fixed first entry
 *
 * @return  a random array
 */
function bx_randomEntries(array, number, noCycle, firstIndex)
{
  if(typeof number == 'object')    // int[] is a range - get random number
    number = number[0] + Math.floor(Math.random()* (number[1]-number[0]+1));
  
  var len = array.length;
  if(len == 0 || number < 1)
    return [];
  if(len == 1)
    noCycle = true;
  
  var entries = [];                // the random entries
  var count = 0;
  var rand;
  var lastIndex = -1;
  var mems = [];                   // boolean array controls cycle
  
  if(typeof firstIndex == 'number' && firstIndex > -1 && firstIndex < len)
  {
    number++;                      // add 1 to include the first 'fixed' value

    entries[count++] = array[firstIndex];        // store entry
    lastIndex = firstIndex;
    mems[firstIndex] = true;
  }
  
  if(noCycle)                      // get random indices - disregard duplicates
  {
    for(var i = count; i < number; i++)
    {
      rand = Math.floor(Math.random()*len);
      entries[i] = array[rand];
    }
    return entries;                // 'pure randoms' ready
  }
  
  // Two seperate loops to generate the random indices
  if(len == 2)
  {
    var forcedFinalIndex = -1;
    
    while(true)                    // len == 2 gives alternating singles and duos
    {
      // Create random index and store new entry
      rand = Math.floor(Math.random()*len);
      entries[count++] = array[rand];
      
      if(count == number)          // loop delimiter
        break;

      if(lastIndex == rand)        // two in a row?
      {
        rand = 1 - rand;           // toggle index
        
        // If we start with a row, force a more cyclic index at the end
        if(count == 2)
          forcedFinalIndex = rand;
        
        entries[count++] = array[rand];
        if(count == number)        // loop delimiter
          break;
      }
      
      lastIndex = rand;
      
      // If asked for at start, force final entry of binary array to differ
      if(count == number-1 && forcedFinalIndex != -1)
      {
        entries[count++] = array[forcedFinalIndex];
        break;
      }
    }
  }
  else                             // len > 2 follows 'random cycle' algorithm
  {
    var isFull = (number % len == 0);            // when all comes full circle
    
    while(count < number)          // loop delimiter
    {
      rand = Math.floor(Math.random()*len);
      
      if(count == 0)
        firstIndex = rand;         // keep the first random index
      else 
        // If there are 2 more to go, and firstIndex has not yet come, but must come..
        if(isFull && count == number-2 && !mems[firstIndex])
          rand = firstIndex;       // ..then force it!
        else 
        {
          var toggle = ((rand + count) % 3 == 0);    // pseudo random toggle
  
          while(mems[rand] || rand == lastIndex ||   // check against duplicates
                (!isFull && count == number-1 && rand == firstIndex))
            if(toggle)
            {
              if(--rand < 0)
                rand = len-1;                        // shift rand back
            }
            else
              if(++rand > len-1)                     // shuffle card forward
                rand = 0; 
        }
  
      // Store random entry
      entries[count++] = array[rand];
      mems[rand] = true;           // keep cycle memory
      lastIndex = rand;
      
      if(count % len == len-1 && count != number)
      {
        // Take the last rand of a cycle to be the last 'still false' entry of mems
        for(var i = 0; i < len; i++)
          if(mems[i])
            mems[i] = false;       // clear cycle memory
          else
          {
            entries[count++] = array[i];
            lastIndex = i;
          }
      }
    }
  }
  return entries;                  // 'cyclic randoms' ready
} 

