/*

javascript.js

JavaScript for http://code.stephenmorley.org/

Created by Stephen Morley - http://stephenmorley.org/ - and released under the
terms of the CC0 1.0 Universal legal code:

http://creativecommons.org/publicdomain/zero/1.0/legalcode

*/

// create the OnloadScheduler object
var OnloadScheduler =
    new function(){

      // store that the scheduled tasks have not yet been executed
      var executed = false;

      // initialise the lists of tasks
      var negativePriority = [];
      var positivePriority = [];

      /* Executes a set of tasks. The parameter is:
       *
       * tasks - an array of tasks to execute. If this optional parameter is
       *         omitted then all scheduled tasks are executed.
       */
      function execute(tasks){

        // check which tasks should be executed
        if (tasks instanceof Array){

          // execute the tasks
          for (var index = 0; index < tasks.length; index ++){
            try{
              tasks[index]();
            }catch (error){
              // ignore the error
            }
          }

        }else if (!executed){

          // store that the scheduled tasks have been executed
          executed = true;

          // execute the tasks
          for (var index = negativePriority.length - 1; index > 0; index --){
            execute(negativePriority[index]);
          }
          for (var index = 0; index < positivePriority.length; index ++){
            execute(positivePriority[index]);
          }

        }

      }

      /* Schedules a task to be executed. The parameters are:
       *
       * task     - the task to be executed, either as a function or as a string
       * priority - the priority of the task - this optional parameter defaults
       *            to 0
       */
      this.schedule = function(task, priority){

        // set the priority to 0 if it was not supplied
        if (!priority) priority = 0;

        // check whether the task has been supplied as a function
        if (task instanceof Function){

          // store the task in the appropriate list
          if (priority < 0){
            if (!negativePriority[-priority]) negativePriority[-priority] = [];
            negativePriority[-priority].push(task);
          }else{
            if (!positivePriority[priority]) positivePriority[priority] = [];
            positivePriority[priority].push(task);
          }

        }else{

          // schedule a function to execute the code string
          this.schedule(function(){eval(task)}, priority);

        }

      }

      // check which method of adding event listeners is supported
      if ('addEventListener' in document){

        // add a DOMContentLoaded event listener
        document.addEventListener('DOMContentLoaded', execute, false);

        // add a load event listener
        window.addEventListener('load', execute, false);

      }else{

        // check that the doScroll function is supported and this is not a frame
        if ('doScroll' in document.documentElement && window == window.top){

          // repeatedly check whether the page can be scrolled
          (function(){
            try{
              document.documentElement.doScroll('left');
              execute();
            }catch (error){
              window.setTimeout(arguments.callee, 0);
            }
          })();

        }

        // add an onreadystatechange event listener
        document.attachEvent(
            'onreadystatechange',
            function(){
              if (document.readyState == 'complete') execute();
            });

        // add a load event listener
        window.attachEvent('onload', execute);

      }

    }();

/* Creates a SmoothMovement. A SmoothMovement produces integer position values
 * representing movement towards a target position, with a maximum acceleration
 * or deceleration of one distance unit per time unit squared. The parameters
 * are:
 *
 * position - the initial position
 * target   - the target position
 */
function SmoothMovement(position, target){

  // initialise the position, target, velocity, and animation interval
  this.position          = position;
  this.target            = target;
  this.velocity          = 0;
  this.animationInterval = null;

}

/* Updates the position an velocity for this SmoothMovement, and returns the
 * new position.
 */
SmoothMovement.prototype.update = function(){

  // check whether the velocity is negative
  if (this.velocity < 0){

    // check whether we must decelerate or can accelerate
    if (this.target > this.position - this.velocity * (this.velocity - 1) / 2){

      // we must decelerate to avoid overshooting, so decrease the speed
      this.velocity ++;

    }else if (this.target <=
        this.position - (this.velocity - 1) * (this.velocity - 2) / 2){

      // we can accelerate without overshooting, so increase the speed
      this.velocity --;

    }

  }else{

    // check whether we must decelerate or can accelerate
    if (this.target < this.position + this.velocity * (this.velocity + 1) / 2){

      // we must decelerate to avoid overshooting, so decrease the speed
      this.velocity--;

    }else if (this.target >=
        this.position + (this.velocity + 1) * (this.velocity + 2) / 2){

      // we can accelerate without overshooting, so increase the speed
      this.velocity++;

    }

  }

  // update the position
  this.position += this.velocity;

  // return the new position
  return this.position;

}

/* Returns true if this SmoothMovement has stopped, and false otherwise. Note
 * that this means that both the velocity and acceleration are zero (or
 * equivalently, that the velocity is zero and the position is at the target).
 */
SmoothMovement.prototype.hasStopped = function(){

  // return whether we have stopped
  return (this.position == this.target && this.velocity == 0);

}

/* Animates this SmoothMovement bycalling the update function repeatedly until
 * the SmoothMovement has stopped. The parameters are:
 *
 * interval       - the interval between updates, in milliseconds
 * updateListener - a function to call after each update. This function is
 *                  passed the new position and the SmoothMovement as its
 *                  first and second parameters.
 * stopListener   - a function to call when the SmoothMovement has stopped. This
 *                  function is passed the SmoothMovement as its parameter. This
 *                  parameter is optional.
 */
SmoothMovement.prototype.animate = function(
    interval, updateListener, stopListener){

  // clear any current animation interval
  if (this.animationInterval) window.clearInterval(this.animationInterval);

  // create the new animation interval
  this.animationInterval = window.setInterval(
      this.createAnimationClosure(updateListener, stopListener), interval);

}

/* Creates a closure for use in the animate function. This function is not
 * intended to be used elsewhere. The parameters are:
 *
 * updateListener - a function to call after each update.
 * stopListener   - a function to call when the SmoothMovement has stopped
 */
SmoothMovement.prototype.createAnimationClosure = function(
    updateListener, stopListener){

  // store a reference to the 'this' object
  var thisObject = this;

  // return the animation closure
  return function(){

    // update the SmoothMovement
    thisObject.update();

    // call the update listener
    updateListener(thisObject.position, thisObject);

    // check whether the SmoothMovement has stopped
    if (thisObject.hasStopped()){

      // clear the animation interval
      window.clearInterval(thisObject.animationInterval);
      thisObject.animationInterval = null;

      // call the stop listener if one was supplied
      if (stopListener) stopListener(thisObject);

    }

  }

}

// create the Website object
var Website =
    {

      /* Creates the header of random bits and creates an interval to flip bits
       * four times a second.
       */
      createHeader : function(){

        // store a reference to the header node
        Website.header = document.getElementById('header');

        // loop over the rows and columns
        for (var row = 0; row < 5; row ++){
          for (var column = 0; column < 80; column ++){

            // create the DOM node for the bit
            var div = document.createElement('div');
            div.style.left = (column * 12) + 'px';
            div.style.top  = (row * 16) + 'px';
            div.style.opacity = 1;

            // randomly assign the bit a 0 or 1 value
            div.appendChild(
                document.createTextNode(Math.floor(2 * Math.random())));

            // add the bit to the header
            Website.header.appendChild(div);

          }
        }

        // set the header background colour
        Website.header.style.backgroundColor = '#ddd';

        // store the list of bits in the header
        Website.headerBits = Website.header.getElementsByTagName('div');

        // create the bit animation interval
        Website.bitAnimationInterval =
            window.setInterval(Website.flipRandomBit, 250);

      },

      /* Selects a random bit from the header and animates it flipping. The
       * selected bit is ignored if it is already in the process of flipping.
       */
      flipRandomBit : function(){

        // select a random bit
        var bit =
            Website.headerBits[
                Math.floor(Website.headerBits.length * Math.random())];

        // check that the bit is not in the process of being flipped
        if (bit.className == ''){

          // mark the bit as being in the process of being flipped
          bit.className = 'flippingOff';

          // start flipping the bit
          Website.createBitFlippingClosure(bit)();

        }

      },

      /* Creates a closure to flip a bit. The parameter is:
       *
       * bit - the DOM node of the bit to flip
       */
      createBitFlippingClosure : function(bit){

        // return the closure
        return function(){

          // check whether the bit is flipping off or flipping on
          if (bit.className == 'flippingOff'){

            // reduce the opacity of the bit, avoiding rounding errors
            bit.style.opacity = Math.round(10 * bit.style.opacity - 1) / 10;

            // check whether the bit is fully transparent
            if (bit.style.opacity == 0){

              // switch the bit to flipping on
              bit.className = 'flippingOn';

              // flip the bit
              bit.firstChild.nodeValue = 1 - bit.firstChild.nodeValue;

            }

          }else{

            // increase the opacity of the bit, avoiding rounding errors
            bit.style.opacity = Math.round(10 * bit.style.opacity + 1) / 10;

          }

          // check whether the bit is back to being opaque
          if (bit.style.opacity == 1){

            // no longer mark the bit as being in the process of being flipped
            bit.className = '';

          }else{

            // continue animating the flip
            window.setTimeout(Website.createBitFlippingClosure(bit), 100);

          }

        }

      },

      /* Handles the page being scrolled by ensuring the navigation is always in
       * view.
       */
      handleScroll : function(){

        // check that this is a relatively modern browser
        if (window.XMLHttpRequest){

          // determine the distance scrolled down the page
          var offset = window.pageYOffset
                     ? window.pageYOffset
                     : document.documentElement.scrollTop;

          // check whether the header has moved into or out of view
          if (offset <= 104 && !Website.bitAnimationInterval){

            // start the header animation
            Website.bitAnimationInterval =
                window.setInterval(Website.flipRandomBit, 250);

            // clear the class on the navigation
            document.getElementById('navigation').className = '';

          }else if (offset > 104 && Website.bitAnimationInterval){

            // stop the header animation
            window.clearInterval(Website.bitAnimationInterval);
            Website.bitAnimationInterval = null;

            // set the class on the navigation
            document.getElementById('navigation').className = 'fixed';

          }

        }

      },

      // define a SmoothMovement instance for use with the navigation animation
      navigationAnimation : new SmoothMovement(896, 80),

      // expands or collapses the navigation
      animateNavigation : function(){

        // store references to the DOM nodes of the toolbar components
        Website.navigationBar = document.getElementById('navigationBar');
        Website.twitterButton = document.getElementById('twitterButton');
        Website.collapseArrow = document.getElementById('collapseArrow');

        // animate the navigation
        Website.navigationAnimation.animate(
            20,
            function(width){

              // set the navigation width
              Website.navigationBar.style.width = width + 'px';

              // move the twitter button
              Website.twitterButton.style.left = (width - 456) + 'px';

              // scale the arrow
              Website.collapseArrow.setAttribute(
                  'src',
                  '/images/collapse-arrow-'
                      + (width > 488 ? 'left' : 'right')
                      + '/');
              Website.collapseArrow.setAttribute(
                  'width',
                  Math.max(1, Math.round(Math.abs((width - 488) / 408 * 20))));

            },
            function(animation){

              // update the animation target and animation button title text
              animation.target = (animation.target == 80 ? 896 : 80);
              document.getElementById('collapseButton').setAttribute(
                  'title',
                  (animation.target == 80
                      ? 'Collapse navigation'
                      : 'Expand navigation'));

            });

      }

    };

// add the event listeners
if (window.addEventListener){
  window.addEventListener('load',   Website.createHeader, false);
  window.addEventListener('scroll', Website.handleScroll, false);
}else{
  window.attachEvent('onload',   Website.createHeader);
  window.attachEvent('onscroll', Website.handleScroll);
  window.attachEvent(
      'onload',
      function(){
        var iframe = document.getElementById('likeButton').getElementsByTagName('iframe')[0];
        var newIframe = iframe.cloneNode(true);  
        newIframe.setAttribute('frameBorder', '0');
        newIframe.style.height='21px';
        iframe.parentNode.replaceChild(newIframe, iframe);
      });
}

