//Beginning Stuff. Necessary! Lots of it is just pieces of Prototype, because FS doesn't need the whole shebang. -----------------------------------
var initT1 = Math.floor(new Date());

Array.prototype.clone = function(deep) {
  if(!deep) return [].concat(this);
  var ret = [];
  for(var i=0; i<this.length; i++)
    ret[i] = this[i].clone?this[i].clone():this[i];
};
Array.prototype.contains = function(obj) {
  for(var i=0; i<this.length; i++)
    if(this[i] == obj) return true;
  return false;
};
Array.prototype.includes = Array.prototype.contains;
Array.prototype.has = Array.prototype.includes;
Array.prototype.indexOf = function(obj) {
  for(var i =0; i<this.length; i++) if(this[i]==obj) return i;
  return -1;
};
Array.prototype.remove = function(obj) {  //removes the first occurrence of this object, not all of them.
  var i = this.indexOf(obj);
  if(i==-1) return null;
  return this.splice(this.indexOf(obj),1)[0];
};
Array.prototype.insert = function(obj,i) {
  for(var j=this.length-1; j>=i; j--) {
    this[j+1] = this[j];
  }
  this[i] = obj;
  return this;
};
Array.prototype.partShifted = function(i,j) {
  var ret = this.clone();
  if(i==j) return ret;
  var temp = ret[i];
  if(i<j) {
    for(var k=i; k<j; k++)
      ret[k] = ret[k+1];
    ret[j] = temp;
  } else {
    for(var k=i; k>j; k--)
      ret[k] = ret[k-1];
    ret[j] = temp;
  }
  return ret;
};
if( !window.$A ) {
  window.$A = function(iterable) {
    if (!iterable) return [];
    if ((!(FS.isSafari) || !(typeof iterable == 'function' && iterable == '[object NodeList]')) && iterable.toArray) {
      return iterable.toArray();
    } else {
      var results = [];
      for (var i = 0, length = iterable.length; i < length; i++)
        results.push(iterable[i]);
      return results;
    }
  };
}

Function.prototype.bind = function() {
  if (arguments.length < 2 && arguments[0]===undefined) return this;
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  };
};

function isNull(obj) {          //Tests using ===
  return obj===null;
}
function isUndefined(obj) {       //Tests using ===
  return obj===undefined;
}
isUndef = isUndefined;
function enumerate(obj, asHTML) {   //Prints out the entire hash of an object. Handy.
  var s = [];
  for(var i in obj)
    s[s.length]=(i+": "+(function() {try{if(typeof obj[i]=='string') return "\""+obj[i]+"\""; return obj[i];}catch(e){return e;}})());
  if(asHTML==true)
    return "\n"+s.join(",\n\n").split("\n").join("<br>").split("  ").join("&nbsp; &nbsp; ")+"\n";
  else return " {\n"+s.join(",\n\n")+"}";
}
function enumerateLite(obj, asHTML) {   //Prints out the entire hash of an object. Handy.
  var s = [];
  for(var i in obj)
    s[s.length]=i;
  if(asHTML==true)
    return "\n"+s.sort().join(",\n").split("\n").join("<br>").split("  ").join("&nbsp; &nbsp; ")+"\n";
  else return " {\n"+s.sort().join(",\n")+"}";
}
function clog(obj) {
  try
  {
    // console.log(obj);
  }
  catch(e)
  {
  }
}
String.prototype.asHTML = function() {
  return this.split('\n').join('<BR>').split("  ").join(" &nbsp;");
};

//End Beginning Junk --------------------------------------------



//Namespace and Sacreds -----------------------------------------
//      These are the only objects not in an FS.Object instance.
//      They are not clonable, dupable, or anything fancy, because they just shouldn't be touched except to read them.

var Framester = {
  maxFPS: 100,
  FPS: 0,
  minFPS: 10,
  robust: true,                   //This will put elapse() calls in try blocks, so that users of Framester won't be able to break the loop with an error.
  merciless: false,               //This only changes anything if Framester is robust: if a task throws an error, it will be removed if Framester is Merciless.
  objectTracking: false,          //Handy, but uses some memory. Maintains a list of all Objects ever created, so you can search by name to get pointers to stray objects.
  preserveCompletedTasks: false,  //Keeps completed Tasks in a "completed" array for future reference. Handy, but a memory hog!
  benchTasks: false,              //Keeps tabs on the computational expenses of Tasks
  allowStatusBoxes: false,        //Status Box ability on Tasks is off by default. Turn it on and call yourTask.statusBox() to get awesome debug!
  showTaskFPSMins: true,          //Decides whether to show on Task toStrings what the minimum FPS of the task is.

  objNumber: 0,
  instanceNumbers: [],
  objects: [],

  host: null,                     //This will hold the WindowBridge to the FS host window.

  taskGroup: null,                //this is the top-level, root TaskGroup, which will handle running all tasks.

  running: false,
  spA: 1,
  preinit: function() {
    FS.setUpBrowserBooleans();
    
    if(window.parent && parent!=window && parent!=null && parent.Framester)
      FS.parentFramester = parent.Framester;
    else if(window.opener && opener!=window && opener!=null && opener.Framester)
      FS.parentFramester = opener.Framester;
    FS.isChildFramester = !!FS.parentFramester;
    
    FS.host = window.FSHost = FS.WindowBridge.create({
      name: "Framester Host",
      window:window
    });
  },
  init: function() {
    FS.taskGroup = FS.TaskGroup.create({
      name: "Framester Root Task Group",
      onframe: function() { FS.FPS = this.minFPS; }
    }).start(null,true);
    FS.tasks = FS.taskGroup.tasks;
    
    if(FS.isChildFramester) {
      if(FS.isSafari && !this.parentFramester.dummySafariFPSDiv) {
        this.parentFramester.dummySafariFPSDiv = this.parentFramester.host.document.createElement('div');
        this.parentFramester.dummySafariFPSDiv.style.cssText = "position:absolute; left:0px; top:0px; width:1px; height:1px; z-index:-100000;";
        this.parentFramester.host.document.body.appendChild(this.parentFramester.dummySafariFPSDiv);
      }
      FS.parentTask = FS.Task.create({
        FS: FS,
        name: "Framester Runner for "+FSHost.document.title+" ("+Math.floor(Math.random()*100000000)+")",
        run: function() {
          this.nextFrame(this.nospawn);
          this.parentTask.minFPS = this.FPS;
          if(FS.isSafari) {
            this.parentFramester.dummySafariFPSDiv.style.left=this.dummybit?"1px":"0px";
            this.dummybit = !this.dummybit;
          }
        }.bind(FS),
        toString: function() {
          var s = arguments.callee.base.call(this);
          s+="---------\n        "+this.FS.taskGroup.toString().split("\n").join("\n       ")+"";
          return s;
        }
      });
    }
        
    FS.start();    
  },

  handleTask: function(task) {
    FS.taskGroup.handleTask(task);
  },

  now: 0,
  lastFrameTime: 0,
  lastFrameFPS: 0,
  averageFPS:0,
  dampedFPS:0,

  lastComputeTime: 0,
  computeTime: 0,
  averageComputeTime: 0,
  dampedComputeTime: 0,
  tenDampedComputeTime: 0,

  elapsedTime: 0,

  frameCount: 0,

  nextTime: -1,

  nospawn: {},    //this is an object pointer, to pass to FS.nextFrame in order to run it ONCE. (true/false won't work, because FF puts in arguments to functions in setTimeout chains)

  nextFrameTimeout: null,

  nextFrame: function(nospawn) {
    if(FS.nextFrameTimeout) {
      clearTimeout(FS.nextFrameTimeout);
      FS.nextFrameTimeout = null;
    }
    if(!FS.running) return;

    var now = FS.realNow();
    var time = now - FS.now;
    var realTime = time;
    if(FS.nextTime!=-1) {
      time = FS.nextTime;
      FS.nextTime = -1;
    }
    FS.now = now;

    FS.taskGroup.elapse(time*FS.spA);

    FS.lastFrameTime = realTime;
    var fps = realTime==0?(1000):(1000/(realTime));
    FS.averageFPS = (FS.averageFPS * FS.frameCount + fps) / (FS.frameCount+1);
    FS.dampedFPS = FS.dampedFPS*.95 + fps*.05;
    FS.lastFrameFPS = Math.floor(100*fps)/100;
    FS.frameCount++;

    var delay = 0;
    var minMillis = 1000/FS.FPS;
    var delay = Math.max(0,minMillis-(FS.realNow()-now));
    if(FS.running && nospawn!==FS.nospawn) FS.nextFrameTimeout = setTimeout(FS.nextFrame,delay);

    FS.elapsedTime+=realTime;

    var finish = FS.realNow();
    FS.lastComputeTime = finish-now;
    FS.computeTime+=finish-now;
    FS.averageComputeTime = (FS.averageComputeTime * FS.frameCount + FS.lastComputeTime) / (FS.frameCount+1);
    FS.dampedComputeTime = FS.dampedComputeTime*.99 + FS.lastComputeTime*.01;
    FS.tenDampedComputeTime = FS.tenDampedComputeTime*.9 + FS.lastComputeTime*.1;
  },

  start: function() {
    if(FS.running) return;
    FS.running = true;
    FS.now = FS.realNow();
    if(FS.isChildFramester) FS.parentTask.start(FS.parentFramester.taskGroup);
    else FS.nextFrame();
  },
  stop: function() {
    if(!FS.running) return;
    FS.running = false;
    if(FS.isChildFramester) FS.parentTask.terminate();
    else if(FS.nextFrameTimeout) {
      clearTimeout(FS.nextFrameTimeout);
      FS.nextFrameTimeout = null;
    }
  },

  /********************Framester Conveniences and Util********************/
  setUpBrowserBooleans: function() {
    if (navigator.appName.toLowerCase().indexOf("explorer")!=-1)
      FS.isIE = true;
    else FS.isIE = false;

    if (!!(navigator.appVersion.match(/\bMSIE.*7\.\b/)))
      FS.isIE7 = true;
    else FS.isIE7 = false;

    FS.isIE6 = FS.isIE7==false && FS.isIE==true;

    if (navigator.userAgent.toLowerCase().indexOf("safari")!=-1)
      FS.isSafari = true;
    else FS.isSafari = false;

    if (FS.isSafari && parseInt(navigator.appVersion.replace(/^.*?AppleWebKit\/(\d+).*?$/,'$1'),0)>420)
      FS.isSafari3 = true;
    else FS.isSafari3 = false ;

    FS.isSafari2 = FS.isSafari==true && FS.isSafari3==false;

    if (navigator.userAgent.toLowerCase().indexOf("gecko")!=-1 && !FS.isSafari)
      FS.isGecko = true;
    else FS.isGecko = false;

    if (navigator.userAgent.toLowerCase().indexOf("mac")!=-1)
      FS.isMac = true;
    else FS.isMac = false;

    if (navigator.userAgent.toLowerCase().indexOf("firefox")!=-1 &&!FS.isSafari)
      FS.isFirefox = true;
    else FS.isFirefox = false;

    if (navigator.userAgent.indexOf('Firefox/2')>=0)
      FS.isFirefox2 = true;
    else FS.isFirefox2 = false;

    if(FS.isFirefox2 && navigator.userAgent.indexOf("Firefox/2.0.0.10")!=-1)
      FS.isFirefox20010 = true;
    else FS.isFirefox20010 = false;

    if (navigator.userAgent.indexOf('Firefox/3')>=0)
      FS.isFirefox3 = true;
    else FS.isFirefox3 = false;

    FS.isFirefox23 = FS.isFirefox2 || FS.isFirefox3;
    FS.isFirefox1 = FS.isFirefox && !FS.isFirefox23;

    if (navigator.userAgent.toLowerCase().indexOf("camino")!=-1)
      FS.isCamino = true;
    else FS.isCamino = false;

    if (!!(navigator.appVersion.match(/(Windows)/)))
      FS.isWindows = true;
    else FS.isWindows = false;

    if (FS.isFirefox && (!!(navigator.appVersion.match(/(Macintosh)/))))
      FS.isMac = true;
    else if (!!(navigator.appVersion.match(/(Mac OS X)/)))
      FS.isMac = true;
    else FS.isMac = false;

    FS.browserSupportsResizableCanvas = (FS.isSafari3 || FS.isGecko) && !FS.isFirefox20010;         //This is for browsers with full canvas support, including dynamic CSS driven resizing of existing prepainted canvas rasters.
    FS.browserSupportsStaticCanvas = FS.browserSupportsResizableCanvas || FS.isSafari || FS.isIE;   //This is for browsers that will paint images to canvases, but only at screen res. No CSS resizing of rasters.
  },

  extraStatus: {},

  status: function(giveAlert) {
    var s = "";
    s+="\nTarget FPS: "+FS.FPS;
    s+="\nMinimum FPS: "+FS.minFPS;
    s+="\nMaximum FPS: "+FS.maxFPS;
    s+="\n\nDamped FPS: "+(Math.floor(100*FS.dampedFPS)/100);
    s+="\n\nLast Frame FPS: "+FS.lastFrameFPS;
    s+=  "\nLast Frame Time: "+FS.lastFrameTime+"ms";
    s+="\n\n10x Damped nextFrame() computation: "+(Math.floor(100*FS.tenDampedComputeTime)/100)+"ms";
    s+=  "\n100x Damped nextFrame() computation: "+(Math.floor(100*FS.dampedComputeTime)/100)+"ms";
    s+=  "\nLast nextFrame() computation: "+FS.lastComputeTime+"ms";
    s+="\n\nAverage nextFrame() computation: "+(Math.floor(100*FS.averageComputeTime)/100)+"ms";
    s+=  "\nAverage FPS: "+(Math.floor(100*FS.averageFPS)/100);
    s+="\n\nUptime: "+(Math.floor(FS.elapsedTime/1000))+" s";
    s+=  "\nTotal Compute Time: "+(Math.floor(FS.computeTime/100)/10)+" s";
    s+=  "\nTotal Frames: "+FS.frameCount;
    s+="\n\nExtra Status Attributes: "+enumerate(FS.extraStatus,true);
    s+="\n\n"+FS.tasks.length+"  Tasks: \n"+FS.tasks.join("\n");
    if(FS.preserveCompletedTasks) s+="\n\n"+FS.completedTasks.length+"  Tasks Complete: \n"+FS.completedTasks.join("\n");
    if(FS.objectTracking) s+="\n\n"+FS.objects.length+"  Objects Created: \n"+FS.objects.join("\n");
    if(giveAlert) alert(s);
    return s;
  },

  perf: function() {
    FS.perfPop = x = window.open("","Framester Perf","height="+(screen.availHeight-19)+",width=500,top=0,left=0");
    x.document.body.innerHTML = '<div style="position:absolute; left:0px; top:0px; width:100%; height:100%; overflow:auto"></div>';
    x.document.body.style.background = "black";
    if(FS.perfPop.updater == null) {
      FS.perfPop.updater = FS.perfPop.setInterval((function() {
        try {
          var s = this.opener.FS.status();
          this.document.body.childNodes[0].innerHTML = "<span style=\"font-family:myriad apple, lucida grande; font-size:12px; color:white;\"><big><big><big>Framester Performance Monitor</big></big></big><br>"+s.asHTML()+"</span>";
        } catch(e) {if(!this.opener || this.opener==this.window) this.close();}
      }.bind(FS.perfPop)),250);
    }
    window.focus();
  },

  currentlySlowMo:false,
  realSpA:1,
  toggleSlow: function() {
    if(FS.currentlySlowMo==true) {
      FS.spA=FS.realSpA;
    }
    else {
      FS.realSpA=FS.spA;
      FS.spA/=10;
    }
    FS.currentlySlowMo = !FS.currentlySlowMo;
  },

  isSafari: null,               //Browser Booleans.
  isSafari2: null,              //For a small, submillisecond price, you get infinite free browser sniffs! w00t!
  isSafari3: null,
  isIE: null,
  isIE6: null,
  isIE7: null,
  isGecko: null,
  isFirefox: null,
  isFirefox2: null,
  isFirefox20010: null,
  isCamino: null,
  isMac: null,
  isWindows: null,

  bound: function(number, upper, lower) {   //Applies Math.max and Math.min
    if(upper<lower) {
      var temp = lower;
      lower = upper;
      upper = temp;
    }
    return Math.max(lower, Math.min(upper, number));
  },

  windowWidth: function() {
    var w = window;
    return w.innerWidth || (w.document.documentElement.clientWidth || w.document.body.clientWidth);
  },
  windowHeight: function() {
    var w = window;
    return w.innerHeight || (w.document.documentElement.clientHeight || w.document.body.clientHeight);
  },

  realNow: function() {return (new Date()).valueOf();},

  setOpacity: function(obj, value) {
    if (!obj) return;
    if (value>=.9999) {
      FS.removeOpacity(obj);
      return;
    }
    obj.style.filter = "alpha(opacity="+Math.round(value*100)+")";
    obj.style.MozOpacity = ""+(Math.round(value*100)/100.0)+"";
    obj.style.opacity = ""+(Math.round(value*100)/100.0)+"";
    obj.fropacity = Math.round(value*100);
  },
  removeOpacity: function(obj) {
    if (!obj) return;
    obj.style.filter = "";
    obj.style.MozOpacity="";
    obj.style.opacity="";
    obj.fropacity = 100;
  },
  getOpacity: function(obj) {
    if (obj.fropacity) return parseInt(obj.fropacity,10)/100.0;
    if (obj.style && obj.style.opacity && obj.style.opacity!="") return Math.round(obj.style.opacity*100)/100.0;
    if (obj.style && obj.style.MozOpacity && obj.style.MozOpacity!="") return Math.round(obj.style.MozOpacity*100)/100.0;
    /* IE Alpha filter handling coming soon. As of now, will just return 1 if other styles aren't defined in IE. */
    obj.fropacity = 100;
    return 1;
  },
  cssPositionRelativity: function(obj) {
    var leftNull = (obj.style.left==null || obj.style.left=="");
    var rightNull = (obj.style.right==null || obj.style.right=="");
    var horzStatus;
    if(leftNull && rightNull)
      horzStatus= "none";
    else if(leftNull)
      horzStatus= "right";
    else if(rightNull)
      horzStatus= "left";
    else {
      horzStatus= "both";
    }

    var topNull = (obj.style.top==null || obj.style.top=="");
    var bottomNull = (obj.style.bottom==null || obj.style.bottom=="");
    var vertStatus;
    if(topNull && bottomNull)
      vertStatus= "none";
    else if(topNull)
      vertStatus= "bottom";
    else if(bottomNull)
      vertStatus= "top";
    else
      vertStatus= "both";

    var a = {h: horzStatus, v: vertStatus};
    if(a.h == "both" || a.h == "none" || a.v == "both" || a.v == "none")
      alert("Error! Vertical or Horizontal arrangement features "+a.h+" of left/right and "+a.v+" of top/bottom!\n"+enumerate(obj));
    return a;
  },
  getCSSPositionNumbers: function(obj, a) {
    if (!obj) {
      alert("Error! Just tried to get the positions of an object from its styles... the object was a null!");
      return;
    }
    if(!a) a = FS.cssPositionRelativity(obj);

    if(a.h=="both" || a.h=="none" || a.v=="both" || a.v=="none") return {x:-2147483647, y:-2147483647};

    eval("var h = obj.style."+a.h);
    var i = h.indexOf("px");
    if (i!=-1)
      h = parseInt(h.substring(0,i),10);
    h = parseInt(h,10);
    eval("var v = obj.style."+a.v);
    i = v.indexOf("px");
    if (i!=-1)
      v = parseInt(v.substring(0,i),10);
    v = parseInt(v,10);

    return {x: h, y: v};
  },

  positionOf: function(element) {                     //cumulative offset from Prototype
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  },

  getPosition: function(elementToGet, left1Top2) {    //this was implemented differently before being replaced with positionOf.
    return FS.positionOf(elementToGet)[left1Top2-1];
  },

  mixin: function(obj,hash) {
    if(!obj || !hash) return;
    for(var i in hash) {obj[i] = hash[i];} return obj;
  },

  toString: function() {
    return "[The Framester Namespace]";
  },
  
  enableStatusBoxForTask: function(task) {
    task._setUpStatusBox = function(x,y) {
      if(!x) x=10;
      if(!y) y=10;
      if(this._statusBoxShowing==true) {
        this._tearDownStatusBox();
        return;
      }
      this._statusBoxShowing = true;
      var boxHeight=30;
      var titleHeight=30;
      this._statusBox = document.createElement("div");
      var pos = (this._statusBoxDraggerPosition==undefined)?({x:x,y:y}):(this._statusBoxDraggerPosition);
      this._statusBox.style.cssText = "position:absolute; left:"+pos.x+"px; top:"+pos.y+"px; width:300px; height:"+boxHeight+"px; overflow:hidden; background:#222222; opacity:.8; color:white; font-family:lucida grande; font-size:11px; z-index:10000000;";
      this._statusBox.innerHTML = '<div style="cursor: move; position:absolute; left:0px; top:0px; width:100%; height:'+titleHeight+'px; overflow:hidden; background:#444444; font-size:14px;"><table style="z-index:1" cellspacing=0 cellpadding=0 border=0 width=100% height=100%><tr><td align=center valign=center style="color:white">'+this.name+'</td></tr></table></div><div style="cursor:default; position:absolute; left:10px; width:280px; top:0px;"></div><div style="position:absolute; left:7px; top:7px; width:11px; height:15px; background:#FF0000; cursor:pointer; border:1px solid white; z-index:2; color:white; line-height:13px; padding-left:4px; opacity:0.5;" onmouseover="FS.fade(this,1,1.3);" onmouseout="FS.fade(this,.5,.5);"><div style="border:1px solid white; width:7px; height:7px; background:white; position:absolute; left:0px; top:0px;"></div></div>';
      this.host.document.body.appendChild(this._statusBox);
      this.frameRun(this._updateStatusBox);
      this._statusBox.lastChild.onmouseup = this._tearDownStatusBox.bind(this);
      this._statusBoxDragger = FS.Draggable.create({
        name: "Status Dragger for "+this.name,
        element: this._statusBox,
        statusSpawner: this,
        onclick: function() {return; if(FSHost.mouse.y>=this.p.y+this.element.childNodes[0].offsetHeight) this.statusSpawner._tearDownStatusBox();},
        init: function() {arguments.callee.base.call(this);this.statusSpawner._statusBoxDraggerPosition = this.p;}
      }).start();
      this._statusBoxTracker = FS.Tracker.create({
        name: "Status Size Tracker for "+this.name,
        f:[{headStart:10,sp:5}],
        render: function() {
          if(parseInt(this.statusSpawner._statusBox.style.height,10)!=this.f[0].c) this.statusSpawner._statusBox.style.height = this.f[0].c+"px";
        },
        statusSpawner: this
      }).start();
      return this;
    };
    task._updateStatusBox = function() {
      this._reallyUpdateStatusBox();
    };
    task._reallyUpdateStatusBox = (function() {
      if(!(this._statusBox && this._statusBox.parentNode)) return;
      this._statusBox.childNodes[1].innerHTML = this.status(true);
      var tH = this._statusBox.firstChild.firstChild.firstChild.firstChild.firstChild.scrollHeight+12;
      if(FS.isFirefox) tH=30;
      var desH = this._statusBox.childNodes[1].scrollHeight+tH+20;
      this._statusBoxTracker.f[0].t = desH;
      if(parseInt(this._statusBox.firstChild.style.height,10)!=tH) this._statusBox.firstChild.style.height = tH+"px";
      if(parseInt(this._statusBox.childNodes[1].style.top,10)!=tH) this._statusBox.childNodes[1].style.top = tH+"px";
    }).bind(task);
    task._callTearerDowner = function() {
      if(!this._statusBoxShowing) return;
      if(this._statusBox) {
        this._statusBox.style.background = "#660000";
        this._statusBox.childNodes[0].style.background = "#AA0000";
      }
      FS.setTimeout(this._tearDownStatusBox.bind(this),750);
    };
    task._tearDownStatusBox = function() {
      if(!this._statusBoxShowing) return;
      if(this._statusBox && this._statusBox.parentNode) this._statusBox.parentNode.removeChild(this._statusBox);
      this.frameStop(this._updateStatusBox);        
      this._statusBox = null;
      this._statusBoxDragger.terminate();
      this._statusBoxDragger = null;
      this._statusBoxTracker.terminate();
      this._statusBoxTracker = null;
      this._statusBoxShowing = false;
    };
    task.finishRun(task._callTearerDowner);
    task.statusBox = task._setUpStatusBox;
  }
  /*******************End FS conveniences****************/
};
var FS = Framester;

if(FS.isSafari)
  FS.positionOf = function(element) {   //cumulative offset from Prototype
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (element.style.position == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return [valueL, valueT];
  };

//End Namespace and Sacreds --------------------------------------------
//      These were the only objects not in an FS.Object instance.








universalFSObjectFunctionality = {
  className: "Object",

  init: function() {                                //This simply sets the name and the fact that the new instance is NOT a class. Every new INSTANCE object gains a fsoid (simply the number of the objects created before it) and an instance id (iid, the number of objects of the same type created before it).
    if(arguments.callee.base instanceof Function) arguments.callee.base.call(this);

    if(!FS.instanceNumbers[this.className]) FS.instanceNumbers[this.className] = 1;
    this.fsoid = FS.objNumber++;
    if(FS.objectTracking == true) FS.objects[this.fsoid] = this;
    this.iid = FS.instanceNumbers[this.className]++;    

    if(!this.name) this.name = "Untitled "+this.className+" "+this.iid;
  },

  set: function(key, value) {
   this[key] = value;
   return this;
  },
  get: function(key) {
   return this[key];
  },

  toString: function() {                            //This is fun. This gets overridden by subclasses, but arguments.callee.base.call allows it to be used to add stuff to the string in subclasses! Look at some subclass toStrings...
    return "["+this.className+": "+this.name+"]";
  },

  _isFS: true
};
if(!window.SproutCore) {
  Function.prototype.create = function() {          //makes a new instance of a class, extending to add on-the-spot mixins if necessary, then running the instance's init function.
    var ret;
    if(!arguments.length) ret = new this();
    else ret = new (this.extend.apply(this,arguments))();
    ret._type = this;
    ret.init();
    return ret;
  };
  Function.prototype.createArray = function(array) {
    array = array.clone();
    for(var i=0, l=array.length; i<l; i++) array[i] = this.create(array[i]);
    return array;
  };
  Function.prototype.extend = function() {          //returns a new function, using the entire superclass as its prototype.
    var ret = function() {};
    var proto = new this();
    for(var i=0, l=arguments.length; i<l; i++)
      proto.mixin(arguments[i]);
    ret.toString = function() {return "[Class: "+this.prototype.className+"]";};
    ret.prototype = proto;
    return ret;
  };
  FS.Object = function() {};                        //this is the base class definition.
  FS.Object.toString = function() {return "[Class: Object]";};
  FS.Object.prototype = {
    clone: function(deep) {                         //clones everything an object has to itself apart from its default form as would be created by its type.
      var tempClass = function() {};
      tempClass.prototype = this._type.prototype;
      var ret = new tempClass();
      if(deep) for(var i in this) {
        if(!this.hasOwnProperty(i)) continue;
        var mine = this[i];
        ret[i]=(mine&&mine.clone)?mine.clone():mine;
      } else for(var i in this) {
        if(!this.hasOwnProperty(i)) continue;
        ret[i]=this[i];
      }
      return ret;
    },
    mixin: function(obj) {                          //does a straight mixin of everything actually on the hash being mixed in; also adds base pointers to overwritten functions
      if(!obj) return;
      for(var i in obj) {
        if(obj.hasOwnProperty(i)) {
          var mine = this[i];
          var its = obj[i];
          if(mine instanceof Function) its.base = mine;
          this[i] = its;
        }
      }
      return this;
    }
  };
  FS.Object.prototype.mixin(universalFSObjectFunctionality);
} else if(window.SproutCore) {
  FS.Object = SC.Object.extend(universalFSObjectFunctionality);
}










Array.prototype.beFun = function(owner) {
  this.owner = owner || window;
  this.runFun = this.runFun.bind(this);
  this.toString = this.stringFun;
  return this;
};
Array.prototype.addFun = function(func) {
  this[this.length] = func;
  return this;
};
Array.prototype.haveFun = function(func) {
  return this.removeFun(func).addFun(func);
};
Array.prototype.removeFun = function(func) {
  for(var i=0; i<this.length; i++) {
    if(this[i]==func) {
      this.splice(i,1);
      i--;
    }
  }
  return this;
};
Array.prototype.runFun = function(e) {
  for(var i=0; i<this.length; i++) {
    var thisFun = this[i];
    if(thisFun instanceof Function) this[i].call(this.owner, e);
    else {
      (function() {
        eval(thisFun);
      }).apply(this.owner);
    }
  }
};
Array.prototype.stringFun = function() {
  return "Fun Array ["+this.join(',')+"]";
};












FS.BoundedArea = FS.Object.extend({                         //Keeps track of four bounds, and, if you use the methods to set them, will keep track of centerpoint and width/height as well.
  className: "BoundedArea",                                 //  Centerpoint is settable, and will reset the bounds to maintain width/height.
  x: null,                                                  //  Ranges are settable, and will reset bounds to maintain centerpoint.
  X: null,
  y: null,
  Y: null,
  c: null,  //set in init
  w: null,  //set in init
  h: null,  //set in init
  dirty: false,
  init: function() {                                        //Init looks for bounds first. If invalid (or nonexistent) bounds, then it'll go off the center/range. If not those, then default values.
    arguments.callee.base.call(this);
    var validBounds = !isNull(this.x) && !isNull(this.X) && !isNull(this.y) && !isNull(this.Y) && this.x<=this.X && this.y<=this.Y;
    if(validBounds==true) {
      this.c = FS.Pair.create({x: this.x+this.X, y: this.y+this.Y}).mult(1/2);
      this.w = this.X-this.x;
      this.h = this.Y-this.y;
    } else if(this.c && this.c.x && this.w && this.c.y && this.h) {
      this.setCenter(this.c);
      this.setRange(this.w, this.h);
    } else {
      this.setBounds({
        x:-1,
        y:-1,
        X:1,
        Y:1
      });
    }
  },
  setRange: function(w, h) {                                //sets range and preserves centerpoint.
    this.w = w;
    this.h = h;
    if(this.c==null) return;
    this.x = this.c.x - w/2;
    this.X = this.c.x + w/2;
    this.y = this.c.y - h/2;
    this.Y = this.c.y + h/2;
    this.dirty = true;
    return this;
  },
  setCenter: function(c,d) {                                //sets centerpoint and preserves range
    if(!isNull(d) && !isUndefined(d)) c = {x:c, y:d};
    if(this.c) {
      if(c.x!=null) this.c.x = c.x;
      if(c.y!=null) this.c.y = c.y;
    } else this.c = c;
    if(!this.c.mag) this.c = FS.Pair.create({x:this.c.x, y:this.c.y});
    if(this.w===null || this.h===null) return;
    this.x = c.x-this.w/2;
    this.X = c.x+this.w/2;
    this.y = c.y-this.h/2;
    this.Y = c.y+this.h/2;
    this.dirty = true;
    return this;
  },
  setTopLeft: function(x,y) {
    if(isNull(y) || isUndefined(y)) {y = x.y; x=x.x;}
    this.x = x;
    this.y = y;
    this.X = x+this.w;
    this.Y = y+this.h;
    if(!this.c) this.c = FS.Pair.create();
    this.c.x = (this.x+this.X)/2;
    this.c.y = (this.y+this.Y)/2;
    this.w  = this.X-this.x;
    this.h  = this.Y-this.y;
    this.dirty = true;
  },
  setBounds: function(bounds) {                             //Sets the bounds manually with a hash, then updates center/ranges.
    this.mixin(bounds);
    if(!this.c) this.c = FS.Pair.create();
    this.c.x = (this.x+this.X)/2;
    this.c.y = (this.y+this.Y)/2;
    this.w  = this.X-this.x;
    this.h  = this.Y-this.y;
    this.dirty = true;
    return this;
  },
  become: function(bA) {
    return this.setBounds({
      x: bA.x,
      y: bA.y,
      X: bA.X,
      Y: bA.y
    });
    this.dirty = true;
  },
  contains: function(x,y,horz1vert2) {
    if(x.className == "Pair") {
      y = x.y;
      x = x.x;
    }
    if((horz1vert2==1 || (this.x<=x && this.X>=x)) && (horz1vert2==2 || (this.y<=y && this.Y>=y))) return true;
    return false;
  },
  toString: function() {
    var s = arguments.callee.base.call(this);
    s = s.substring(0,s.length-1);
    return s+" x="+this.x+" y="+this.y+" X="+this.X+" Y="+this.Y+" w="+this.w+" h="+this.h+" c="+this.c.toTinyString()+"]";
  },
  fitRect: function(w,h) {
    var a = w/h;
    var tA = this.w/this.h;
    var retW, retH, left, top;
    if(a>tA) {
      retW = this.w;
      retH = retW/a;
      left = 0; top = (this.b.h - retH)/2;
    } else {
      retW = this.h*a;
      retH = this.h;
      left = (this.w.h - retW)/2; top = 0;
    }
    return {w:retW, h:retH, l:left, t:top};
  },
  toTinyString: function() {
    return "[("+this.x+","+this.y+"),("+this.X+","+this.Y+")]";
  }
});

FS.Pair = FS.Object.extend({                                //Simple ordered pair, with a few perks. This will be very slow with all the cloning going on in a lot of these methods. Must improve.
  className: "Pair",
  x: null,
  y: null,
  equals: function(p) {
    return p.x == this.x && p.y == this.y;
  },
  distanceFrom: function(p) {
    var dx = p.x - this.x, dy = p.y - this.y;
    return Math.sqrt(dx*dx+dy*dy);
  },
  mag: function() {
    return this.distanceFrom({x:0,y:0});
  },
  rotation: function(theta, center) {                       //Returns a version of the point rotated theta radians around the supplied centerpoint.
    var q = this.added({x: 0-center.x, y: 0-center.y});
    var x = q.x * Math.cos(theta) - q.y * Math.sin(theta);
    var y = q.x * Math.sin(theta) + q.y * Math.cos(theta);
    q.x = x;
    q.y = y;
    q.add(center);
    return q;
  },
  unitVector: function(p) {
    if(!p) p={x:0, y:0};
    var d = this.distanceFrom(p);
    var v = {x: this.x-p.x, y: this.y-p.y};
    return FS.Pair.create({x:v.x/d, y:v.y/d});
  },
  add: function(p) {
    if(!p) return this;
    if(Math.floor(p)-Math.floor(p)+5==5) p = {x: p, y: p};  //Changes a scalar to a vector. A way to check this better?
    this.x += p.x;
    this.y += p.y;
    return this;
  },
  added: function(p) {
    var q = this.clone();
    if(!p) return q;
    q.add(p);
    return q;
  },
  mult: function(p) {
    if(!p) return this;
    if(Math.floor(p)-Math.floor(p)+5==5) p = {x: p, y: p};  //Changes a scalar to a vector. A way to check this better?
    this.x *= p.x;
    this.y *= p.y;
    return this;
  },
  multed: function(p) {
    var q = this.clone();
    if(!p) return q;
    q.mult(p);
    return q;
  },
  negated: function() {
    var q = this.clone();
    q.x = 0-q.x;
    q.y = 0-q.y;
    return q;
  },
  inverted: function() {
    var q = this.clone();
    q.x = 1/q.x;
    q.y = 1/q.y;
    return q;
  },
  become: function(x,y) {
    this.x = x;
    this.y = y;
  },
  toString: function() {                                    //Upgrades Object's toString by also giving the values in the Pair.
    var s = arguments.callee.base.call(this);
    s = s.substring(0,s.length-1)+" ("+(Math.round(this.x*100)/100)+", "+(Math.round(this.y*100)/100)+")]";
    return s;
  },
  toTinyString: function() {
    return "("+(Math.round(this.x*100)/100)+","+(Math.round(this.y*100)/100)+")";
  }
});












FS.WindowBridge = FS.Object.extend({
  className: "WindowBridge",

  window: null,         //required
  document: null,         //created; anything here at init is overwritten with window's document.

  resizeFun: null,        //created: all these functions will run on window resize
  mouseMoveFun: null,       //created: all these functions will run on mousemove in the document
  mouseUpFun: null,       //created: all these functions will run on mouseup in the document
  mouseOutFun: null,        //created: all these functions will run on mouseout in the document
  mouseDownFun: null,       //created: all these functions will run on mousedown in the document

  mouse: null,          //created: Pair with last mouse coordinates from any mouse interaction.
  bounds: null,         //created: BoundedArea with last window size

  init: function() {
    arguments.callee.base.call(this);
    if(!this.window) {
      // clog("Window required in order to create WindowBridge instance."); 
      return;
    }
    //pointer stuff
    this.document = this.window.document;
    //initialize fun piles
    this.resizeFun = [].beFun(this);
    this.mouseMoveFun = [].beFun(this);
    this.mouseUpFun = [].beFun(this);
    this.mouseOutFun = [].beFun(this);
    this.mouseDownFun = [].beFun(this);
    // set up stats
    this.mouse = FS.Pair.create({x:-1,y:-1});
    this.bounds = FS.BoundedArea.create({x:0,y:0,X:1,Y:1});
    //attaching updaters to fun piles
    this.updateBounds = this.updateBounds.bind(this);
    this.updateMouse = this.updateMouse.bind(this);
    this.mouseMoveFun.addFun(this.updateMouse);
    this.mouseUpFun.addFun(this.updateMouse);   
    this.mouseOutFun.addFun(this.updateMouse);    
    this.mouseDownFun.addFun(this.updateMouse);
    this.resizeFun.addFun(this.updateBounds);
    //take care of initial values
    this.updateBounds();
  },

  simpleAttach: function(w) {     //this attaches the window/document-wide triggers to the window/document in a way that overrides anything else, but that works for Framester. Not generally to be used except for simple or enclosed apps.
    if(w) {
      this.window = w;
      this.document = w.document;
    }
    this.window.onresize = this.resizeFun.runFun;
    this.document.onmousemove = this.mouseMoveFun.runFun;
    this.document.onmouseout = this.mouseOutFun.runFun;
    this.document.onmouseup = this.mouseUpFun.runFun;
    this.document.onmousedown = this.mouseDownFun.runFun;
    return this;
  },

  prototypeAttach: function(w) {      //this attaches the window/document-wide triggers to the window/document using Prototype's magicks.
    if(w) {
      this.window = w;
      this.document = w.document;
    }
    this.window.Event.observe(this.window,'resize',this.resizeFun.runFun);
    this.window.Event.observe(this.document,'mousemove',this.mouseMoveFun.runFun);
    this.window.Event.observe(this.document,'mouseout',this.mouseOutFun.runFun);
    this.window.Event.observe(this.document,'mouseup',this.mouseUpFun.runFun);
    this.window.Event.observe(this.document,'mousedown',this.mouseDownFun.runFun);
    return this;
  },

  updateMouse: function(e) {
    if (!e) e = this.window.event;
    var xx,yy;
    if (e) {
      xx = e.clientX;
      yy = e.clientY;
    }
    if(!(xx&&yy)) return; //Drags that query the mouse coords will get what the last coords were.
    this.mouse.x = xx;
    this.mouse.y = yy;
  },

  updateBounds: function() {
    try{
      this.bounds.setBounds({
        x: 0,
        X: this.windowWidth(),
        y: 0,
        Y: this.windowHeight()
      });
    }
    catch(e) {alert("There was an error setting the bounds, justifying this try block in WindowBridge...\nThe error was an "+e); setTimeout(this.updateBounds,10);}
  },

  windowWidth: function() {
    var w = this.window;
    return w.innerWidth || (w.document.documentElement.clientWidth || w.document.body.clientWidth);
  },
  windowHeight: function() {
    var w = this.window;
    return w.innerHeight || (w.document.documentElement.clientHeight || w.document.body.clientHeight);
  }
});








FS.preinit();


















FS.Task = FS.Object.extend({  //This is what gets added to Framester's queue of things to do. It handles termination, statistic tracking, pausing, speed adjust, and priority/importance in the queue (what gets run before what else).
  className: "Task",
  allowStatusBox: FS.allowStatusBoxes,
  host: null,                 //This is a pointer to the WindowBridge connecting to the window in which Framester resides. Can be overridden. Defaults to FSHost.
  running: false,
  done: false,
  paused: false,
  frameCount: 0,
  elapsedTime: 0,
  adjustedComputeTime: 0,
  computeTime: 0,
  lastComputeTime: 0,
  dampedLastComputeTime: 0,
  importance: 0,              //Would've made this "priority," but I wanted it to be intuitive with higher numbers meaning sooner execution. Importance is not yet implemented in FS.handleTask(task) though.
  minFPS: FS.minFPS,          //Default Minimum FPS is low.
  spA: 1,                     //This is just the Task-level speed adjustment coefficient. spA is easier to type!
  tgroup: null,               //The default group will be FS.taskGroup, but can be changed. If this.start(groupX) is called, this.tgroup will be changed to reflect the new group.

  init: function() {          //Defines frame/start/finish/pauseFun and adds any functions with "onframe," "onstart," etc. into those Fun Arrays, respectively. This way, a function "onFrame" will be run every frame.
    arguments.callee.base.call(this);
    if(!this.host) this.host = FSHost;
    if(!this.tgroup) this.tgroup = FS.taskGroup;
    this.startFun = [].beFun(this);
    this.frameFun = [].beFun(this);
    this.finishFun = [].beFun(this);
    this.pauseFun = [].beFun(this);
    
    for(var i in this) {
     var it = this[i];
     if(i.toLowerCase().indexOf("onframe")!=-1 && (typeof it=="function" || typeof it=="string")) this.frameFun.addFun(it);
     if(i.toLowerCase().indexOf("onstart")!=-1 && (typeof it=="function" || typeof it=="string")) this.startFun.addFun(it);
     if(i.toLowerCase().indexOf("onfinish")!=-1 && (typeof it=="function" || typeof it=="string")) this.finishFun.addFun(it);
     if(i.toLowerCase().indexOf("onpause")!=-1 && (typeof it=="function" || typeof it=="string")) this.pauseFun.addFun(it);
    }

    if(this.allowStatusBox) FS.enableStatusBoxForTask(this);
  },

  frameRun: function() {if(arguments.length==0) return; if(!this.frameFun) this.frameFun = [].beFun(this); for(var i=0, l=arguments.length; i<l; i++) this.frameFun.addFun(arguments[i]);},
  startRun: function() {if(arguments.length==0) return; if(!this.startFun) this.startFun = [].beFun(this); for(var i=0, l=arguments.length; i<l; i++) this.startFun.addFun(arguments[i]);},
  pauseRun: function() {if(arguments.length==0) return; if(!this.pauseFun) this.pauseFun = [].beFun(this); for(var i=0, l=arguments.length; i<l; i++) this.pauseFun.addFun(arguments[i]);},
  finishRun: function() {if(arguments.length==0) return; if(!this.finishFun) this.finishFun = [].beFun(this); for(var i=0, l=arguments.length; i<l; i++) this.finishFun.addFun(arguments[i]);},
  
  start: function(tgroup, doNotHandle) {
    this.startFun.runFun();
    this.running = true;
    this.done = false;
    if(!doNotHandle) this.fsHandle(tgroup);
    return this;
  },
  fsHandle: function(tgroup) {
    if(!tgroup) tgroup = this.tgroup;
    else this.tgroup = tgroup;
    tgroup.handleTask(this);
    return this;
  },
  elapse: function(time) {      //Framester's nextFrame calls "elapse" which keeps track of stats and calls "run," which is where all the fun subclasses do their magic.
    if(this.done==true || this.running==false || this.paused==true) return;
    time*=this.spA;
    this.frameCount++;
    this.elapsedTime += time;
    if(FS.benchTasks) {
      var start = FS.realNow();
      this.run(time);
      this.frameFun.runFun();
      var end = FS.realNow();
      this.lastComputeTime = end-start;
      this.dampedLastComputeTime = .95*this.dampedLastComputeTime+.05*this.lastComputeTime;
      this.computeTime += end-start;
      this.adjustedComputeTime += this.spA*(end-start);
    } else {
      this.run(time);
      this.frameFun.runFun();
    }
    return this;
  },
  pause: function() {
    if(this.paused==true) return;
    this.pauseFun.runFun();
    this.paused = true;
    return this;
  },
  resume: function() {
    if(this.paused==false) return;
    this.pauseFun.runFun();
    this.paused = false;
    return this;
  },
  toggle: function() {
    if(this.paused) this.resume();
    else this.pause();
    return this;
  },
  terminate: function() {         //The difference between terminate and die is that terminate runs the finishFun group. If you want something gone without any peep, tell it to die. If it needs to clean up, terminate's your friend.
    if(this.running && this.finishFun) this.finishFun.runFun();
    this.die();
    return this;
  },
  die: function() {
    this.done = true;
    this.running = false;
    this.paused = false;
    return this;
  },
  handleInterrupt: function() {   //This is what happens when a Task with the same name is added to the Framester queue and displaces this one. Put any cleanup in this method.
    this.running = false;
    return this;
  },

  run: function(time) {           //Override this function.
    return this;
  },

  status: function(asHTML, giveAlert) { //This is fun! Status string that's upgradeable with arguments.callee.base.call! Just call status() on your task to see a readout of everything important!
    var s = "";
    s+="\nName: "+this.name+"\n";
    s+="Type: "+this.className+"\n";
    s+="--------\n";
    s+="Minimum Required FPS: "+this.minFPS+"\n";
    s+="Frame Count: "+this.frameCount+"\n";
    s+="Elapsed Time: "+(Math.round(this.elapsedTime*10)/10000)+" s\n";
    if(FS.benchTasks) {
      s+="Total Compute Time: "+(this.computeTime/1000)+" s\n";
      s+="--------\n";
      s+="Compute Time: "+(Math.round(this.dampedLastComputeTime*100)/100)+" ms\n";
      s+="Percentage of Optimal Frametime: "+(Math.round(10*(this.dampedLastComputeTime)/(1000/FS.FPS)*100)/10)+"\n";
      s+="Percentage of Current Frametime: "+(Math.round(10*(this.dampedLastComputeTime)/(FS.lastFrameTime)*100)/10)+"\n";
    }
    s+="--------\n";
    s+="Running: "+this.running+"\n";
    s+="Done: "+this.done+"\n";
    s+="Paused: "+this.paused+"\n";
    s+="--------\n";
    s+="Importance: "+this.importance+"\n";
    s+="Speed Adjust: "+this.spA+"\n";
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  },

  toString: function() {
    var s=arguments.callee.base.call(this);
    return s.substring(0,s.length-1)+(FS.showTaskFPSMins?(" fps="+this.minFPS):"")+(FS.benchTasks?(" cost="+this.computeTime+"ms"):"")+"]";
  }
});


















FS.TaskGroup = FS.Task.extend({
  className: "TaskGroup",
  tasks: null,
  completedTasks: null,
  robust: FS.robust,
  merciless: FS.merciless,
  preserveCompletedTasks: FS.preserveCompletedTasks,
  init: function() {
    arguments.callee.base.call(this);
    this.tasks = [];
    this.completedTasks = [];
  },

  handleTask: function(task) {
    for(var i=0; i<this.tasks.length; i++) {
      if(this.tasks[i]==task) return;
      if(this.tasks[i].name == task.name) {
        this.tasks[i].handleInterrupt();
        this.tasks.splice(i,1);
      }
    }
    if(task.importance>0) {
      for(var i=0; i<this.tasks.length; i++)
        if(this.tasks[i].importance>=task.importance)
          break;
      this.tasks.insert(task,i);
    } else this.tasks.push(task);
  },
  removeTask: function(task) {
    for(var i=0; i<this.tasks.length; i++)
      if(this.tasks[i]==task) return this.removeTaskByIndex(i);
    return null;
  },
  removeTaskByIndex: function(i) {
    if(i>=this.tasks.length || i<0) return null;
    var ret = this.tasks.splice(i,1);
    if(this.preserveCompletedTasks)
      this.completedTasks.push(ret);
    return ret;
  },

  run: function(time) {
    for(var i=0; i<this.tasks.length; i++) {
      if(this.tasks[i].done!=true) {
        if(this.robust==false) this.tasks[i].elapse(time);
        else {
          try {this.tasks[i].elapse(time);}
          catch(e) {
            if(this.merciless)
              this.tasks[i].die();
          }
        }
      }
    }
    var nextFPS = FS.minFPS;
    for(var i=0; i<this.tasks.length; i++) {            //Remove dead tasks and figure out the new minimum FPS for this task group. Kills two birds with one loop.
      var task = this.tasks[i];
      if(!task.done && !task.paused) nextFPS = Math.max(nextFPS,task.minFPS);
      else this.removeTaskByIndex(i);
    }
    this.minFPS = nextFPS;
  },

  status: function(asHTML, giveAlert) {                 //This is fun! Status string that's upgradeable with arguments.callee.base.call! Just call status() on your task to see a readout of everything important!
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+=this.tasks.length+" Task"+(this.tasks.length!=1?"s":"")+":\n"+this.tasks.join("\n ");
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  },

  toString: function() {
    return arguments.callee.base.call(this)+"\n          "+this.tasks.join("\n          ");
  }
});
























/*
Assumptions:
  X == "don't care"
  a==0 || a==-1 || a==1
  sp > 0
  headStart > 0
  a==0           =>   headStart = X
  s==null          =>   a!=1
  t==null          =>   a!=-1 && a!=1 && start==null
  t==null && s==null     =>   persistent == true

Intentions:
  * This is a special type of Task, the only kind that doesn't have it's elapse(time) called directly from Framester's nextFrame.
    It is intended that this be called from the Animation or Tracker that employs the Frumber.
*/
FS.Frumber = FS.Task.extend({   //By default, a simple one-second zero-to-one-then-terminate Frumber will be created. Pass a hash to create() to customize.
  className: "Frumber",
  owner: null,
  s: 0,                     //start value
  c: 0,                     //current value
  t: 1,                     //target value
  a: 0,                     //acceleration preference (will add, perhaps, -2 for "custom" and have it divert to a function in this case! This way you can have Frumbers with "c" defined as a function of time!)
  sp: 1,                    //speed coefficient
  min: null,                //if null, does not apply. This prevents c from EVER going below min.
  max: null,                //if null, does not apply. This prevents c from EVER going above max.
  headStart: 0,             //head start for acceleration or deceleration in the exponential model (Without one, it takes a LONG time to start or stop. Remember the lag time problem with Hi-res assets coming up in Carousel?)
  persistent: false,        //If true, the Frumber won't terminate when it reaches its target. This turns it into the equivalent of last week's Fracker, to be used in a Tracker instead of an Animation.
  ctequal: true,            //This doesn't affect the internal computation; it's determined by it. It's like an OUTPUT. You can look at it, and it'll tell you whether it was at the target or not at after the last frame.

  init: function() {        //Some validation checking, namely to enforce the assumptions above. Probably more to do here.
    arguments.callee.base.call(this);
    this.headStart = Math.max(0.01,Math.abs(this.headStart));
    if(!isNull(this.t)) this.sp = Math.abs(this.sp);
    this._firstS = this.s;
    this._firstC = this.c;
    this._firstT = this.t;
    this._firstA = this.a;
    this._firstSP = this.sp;
  },

  run: function(time) {     //Simply branches out the run code depending on what type of Frumber it is. (Types are: start-to-target traditional (SCT), current-to-target fracker (CT), perpetually linearly changing single number (C))
    if(this.paused==true || this.done==true || this.running==false) return;
    if(!isNull(this.min) && this.t<this.min) this.t = this.min;     //Enforce the min
    if(!isNull(this.max) && this.t>this.max) this.t = this.max;     //Enforce the max
    if(isNull(this.s) && isNull(this.t))        this.runC(time);    //This is a simple ever-increasing targetless number.
    else if(isNull(this.s) && !isNull(this.t))  this.runCT(time);   //This is last week's Fracker. No start value.
    else if(!isNull(this.s) && !isNull(this.t)) this.runSCT(time);  //This is a traditional Frumber: start to finish, then terminate if persistent is false.
    if(!isNull(this.min) && this.c<this.min) this.c = this.min;     //Enforce the min
    if(!isNull(this.max) && this.c>this.max) this.c = this.max;     //Enforce the max
    return this;
  },
  runC: function(time) {      //assumes a==0s
    this.c += this.sp*time/1000;
  },
  runCT: function(time) {     //assumes a==-1 || a==0
    this.runSCT(time);        //Just run the more capable function. Because a!=1, this.s will never be used.
  },
  runSCT: function(time) {    //assumes a==-1 || a==0 || a==1
    var wasLess = this.c <  this.t;
    var hsOffset = ((wasLess==true && this.a==-1) || (wasLess==false && this.a==1))?(this.headStart):(-1*this.headStart);
    var isEqual = this.c == this.t;
    if(isEqual==false) {
      var delta;
      if(this.a == -1)
        delta = time/1000*this.sp*Math.abs(this.t+hsOffset-this.c);
      if(this.a == 1)
        delta = time/1000*this.sp*Math.abs(this.s+hsOffset-this.c);
      if(this.a == 0)
        delta = Math.abs(this.sp*time/1000);
      if(wasLess) this.c += delta;
      else        this.c -= delta;
    }
    if(isEqual && !this.ctequal) this.ctequal = true;
    else if(!isEqual && this.ctequal) this.ctequal = false;
    var isDone = (wasLess && this.c>=this.t) || (!wasLess && this.c<=this.t);
    if(isDone) {
      this.c = this.t;
      if(this.persistent==false) this.terminate();
    }
  },

  atTarget: function() {return this.c==this.t;},      //This is some old Fracker functionality that I cannot live without. More functionality like this will be coming soon.
  nearTarget: function(err) {return Math.abs(this.t-this.c)<=err;},

  reverse: function() {                               //If SCT Frumber, swaps start/target. If CT Frumber (like Fracker), does nothing (can't reverse a tracker!). If a C Frumber (targetless) simply negates speed.
    if(this.s===null && this.t===null) this.sp*=-1;
    else if(!(this.s===null) && !(this.t===null)) {
      var x = this.s;
      this.s = this.t;
      this.t = x;
    }
    return this;
  },
  reset: function() {                                 //Resets Frumber to initial values, but does NOT reset pause/done/running information.
    this.s  = this._firstS;
    this.c  = this._firstC;
    this.t  = this._firstT;
    this.a  = this._firstA;
    this.sp = this._firstSP;
    return this;
  },
  status: function(asHTML, giveAlert) {               //Fun! "Upgrades" the Object's status function by actually calling it, then adding onto it!
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+="Start: "+(Math.round(100*this.s)/100)+"\n";
    s+="Current: "+(Math.round(100*this.c)/100)+"\n";
    s+="Target: "+(Math.round(100*this.t)/100)+"\n";
    s+="Accel: "+this.a+"\n";
    s+="Speed: "+(Math.round(100*this.sp)/100)+"\n";
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  },
  toString: function() {                              //Fun! Upgrades Object's toString function by calling it, then adding useful information to it!
    var s = arguments.callee.base.call(this);
    s = s.substring(0,s.length-1);
    s+=" "+(this.running?"R":"r")+(this.done?"D":"d")+(this.paused?"P":"p");
    s+=" s="+(Math.round(100*this.s)/100)+" c="+(Math.round(100*this.c)/100)+" t="+(Math.round(100*this.t)/100)+"]";
    return s;
  }
});

FS.UnitFrumber = FS.Frumber.extend({
  className: "UnitFrumber",
  duration: 1,
  init: function() {
    arguments.callee.base.call(this);
    this.sp = 1/this.duration;
    this._firstSP = this.sp;
  }
});






















// clog("fix speed adjust in Animation instances... it doesn't seem to pass down to the Frumber properly");

FS.Animation = FS.Task.extend({
  className: "Animation",
  f: null,                          //User supplies hashes that will be used to create customized Frumbers. Default is a single one-second Frumber that does 0-1-terminate.
  persistent: false,                //If persistent is true, Animation will terminate at any point when all its Frumbers have terminated. If false, it will live on like a Tracker.
  minFPS: FS.maxFPS,
  init: function() {                //Goes through the list of Frumber hashes and makes Frumbers out of them. Also adds any "render" function to frameFun.
    arguments.callee.base.call(this);
    if(!this.f) this.f = [{}];
    if(!this.f.length) this.f = [this.f];
    for(var i=0; i<this.f.length; i++) {
      this.f[i] = FS.Frumber.create(this.f[i]);
      this.f[i].mixin({persistent: this.persistent, owner: this});                                //Dang! I need to be able to pass more than one hash into create methods... That's annoying.
      this.f[i].start(null,true);
    }
    if(this.render!=null && typeof this.render=="function") this.frameFun.addFun(this.render);    //Adds a render function to Task's list of things to do on every frame. Just a convenience.
  },
  onstartmakefrumbersgo: function() {   //Starts all the Frumbers so they don't drag butt.
    for(var i=0; i<this.f.length; i++)
      this.f[i].start(null,true);
  },
  run: function(time) {                 //Elapses all the Frumbers (which are also Tasks, only in the animation instead of in Framester's queue), and terminates if persistent is true and they're all done.
    if(!this.running || this.paused || this.done) return;
    var allDone = true;
    for(var i=0; i<this.f.length; i++) {
      this.f[i].elapse(time);
      if(this.f[i].done==false) allDone=false;
    }
    if(allDone && !this.persistent) this.terminate();
    return this;
  },
  reverse: function() {                 //Simply reverses all the Frumbers.
    for(var i=0; i<this.f.length; i++)
      this.f[i].reverse();
    return this;
  },
  reset: function() {                   //Resets the Animation back to its initial state (BUT DOES NOT reset paused/running/done information!). Simply resets all Frumbers.
    for(var i=0; i<this.f.length; i++) {
      this.f[i].reset();
    }
    return this;
  },
  finish: function() {                  //Runs all render methods and onframes, etc, without allowing them to tell the animation to finish again (this caused infinite recursion before I fixed it!)
    if(this.finishing) return;
    this.finishing = true;
    for(var i=0; i<this.f.length; i++) this.f[i].c = this.f[i].t;
    this.frameFun.runFun();
    this.terminate();
    this.finishing = false;
  },
  terminate: function() {               //Terminates all the Frumbers (so they're all at their target states) and then terminates normally, going back to the Task terminate and running frameFun and setting done=true.
    for(var i=0; i<this.f.length; i++)
      this.f[i].terminate();
    arguments.callee.base.call(this);
    return this;
  },
  status: function(asHTML, giveAlert) { //Upgrades Object's status with useful tidbits related to Animations.
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+="Persistent: "+this.persistent+"\n";
    s+="Frumbers: \n"+this.f.join("\n")+"\n";
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  }
});

































FS.Fracker = FS.Frumber.extend({        //TRIUMPH! A Fracker is just a customization of a Frumber that has no start value and is persistent! It's just a CT Frumber!
  className: "Fracker",                 //A default, unmodified Fracker starts with current == target == 0, with exponential decay as acceleration.
  persistent: true,                     //Frackers (or any CT Frumbers) can have a==-1 or a==0, but NOT a==1, because a start value is required for exponential acceleration. Hard to conceptualize.
  s: null,
  t: 0,
  a:-1,
  isStatic: false,                      //isStatic is read-only. If you see that it's true, it means that after the last frame, all frackers were at their targets.
  toString: function() {                //Doesn't upgrade Frumber's toString... upgrade's Task's toString with .base.base.call. It doesn't show the start value (which is always null). Frumber's does.
    var s = arguments.callee.base.base.call(this);
    s = s.substring(0,s.length-1);
    s+=" "+(this.running?"R":"r")+(this.done?"D":"d")+(this.paused?"P":"p");
    s+=" c="+(Math.round(100*this.c)/100)+" t="+(Math.round(100*this.t)/100)+"]";
    return s;
  }
});

FS.Tracker = FS.Task.extend({
  className: "Tracker",
  f: null,                              //List o' Frackers. f is for short.
  minFPS: FS.minFPS,
  init: function() {                    //Generates Frackers. Default is one single default Fracker with c=t=0 and a=-1 for exponential decay tracking.
    arguments.callee.base.call(this);
    if(!this.f) this.f = [{}];
    if(!this.f.length) this.f = [this.f];
    for(var i=0; i<this.f.length; i++) {
      this.f[i] = FS.Fracker.create(this.f[i]);
      this.f[i].mixin({owner: this});
    }
    if(this.render!=null && typeof this.render=="function") this.frameFun.addFun(this.render);
  },
  onstartmakefrackersgo: function() {
    for(var i=0; i<this.f.length; i++)
      this.f[i].start(null,true);
  },
  run: function(time) {                 //Elapses the Frackers.
    if(!this.running || this.paused || this.done) return;
    var nextFPS = FS.minFPS;
    for(var i=0; i<this.f.length; i++) {
      this.f[i].elapse(time);
      if(!this.f[i].ctequal) nextFPS = FS.maxFPS;
    }
    this.isStatic = FS.minFPS == nextFPS;
    this.minFPS = nextFPS;
    return this;
  },
  finish: function() {
    for(var i=0; i<this.f.length; i++) this.f[i].c = this.f[i].t;
  },
  status: function(asHTML, giveAlert) { //Just tosses the toStrings of all the Frackers onto the status given by Task.
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+="isStatic: "+this.isStatic+"\n";
    s+="--------\n";
    s+="Frackers: \n"+this.f.join("\n")+"\n";
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  }
});





































FS.Runner = FS.Task.extend({                      //Implements setTimeout and setInterval (with some cool features added on that you don't have to care about!)
  className: "Runner",
  taskFun: null, //will be a Fun Array            //Instead of a single function, you can have a bunch run. Instead of running a function task(), it runs taskFun.runFun().
  startTime: 0,                                   //Interval will not start until startTime has elapsed.
  endTime: (1e+50),                               //Interval will run until endTime has elapsed. Default is ... indefinite enough.
  maxRuns: (1e+50),                               //Interval will run until maxRuns runs have occurred. If endTime occurs first, note that maxRuns will never be reached.
  maxSimultaneousRuns: 10,                        //If a nonzero interval is faster than the framerate, the tasks will be run more than once per frame, to simulate a quicker interval. This limits the maximum simultaneous runs per frame. Zero => no limit.
  interval: 0,                                    //Interval of zero implies running once per frame. Nonzero intervals are in milliseconds. If the framerate is too slow, the tasks will be run as many times as intervals occurred since the last frame to make up for lost time.
  shouldTerminate: function() {return false;},    //A customizeable function that is run every frame. If it's ever true, the Runner will self-terminate.
  lastTaskTime: 0,                                //DON'T TOUCH! :)
  mathyRunCount: 0,                               //DON'T TOUCH! :) If you never change the interval, this will be the number of actual task executions. If you do, this changes to keep things fluid.
  runs: 0,                                        //This is the real run count, which is incremented appropriately. Query this if you want to know how many times it's run.
  usedFrames: 0,                                  //... um... REALLY DON'T TOUCH this one! :)
  lastInterval: null,                             //in init, will set to interval's value.    //Don't touch this, either. It's here so you can directly change the interval on the runner without weird stuff happening.
  init: function() {                              //Initializes taskFun and adds any function called "task" into it to emulate the old single-function functionality.
    arguments.callee.base.call(this);
    this.lastInterval = this.interval;
    this.taskFun = [].beFun(this);
    if(this.task!=null && (typeof this.task=="function" || typeof this.task=="string")) this.taskFun.addFun(this.task);
  },
  run: function(time) {                           //I don't even remember all the pain I had to go through to make this work right. If you want to dissect it, feel free. ;-)
    if(this.paused || this.done || !this.running) return;
    var neededRunsIfIntervalled = Math.ceil((Math.min(this.endTime,this.elapsedTime)-this.startTime)/this.interval);
    if(this.lastInterval!=this.interval) {
      this.mathyRunCount = neededRunsIfIntervalled-1;
      this.lastInterval = this.interval;
    }
    if((this.elapsedTime>=this.startTime && this.elapsedTime<=this.endTime) || (this.interval!=0 && neededRunsIfIntervalled>this.mathyRunCount)) {
      var taskString = false;
      if(typeof this.task=="string") taskString = true;
      if(this.interval!=0) {
        var neededRuns = neededRunsIfIntervalled;
        var runs = neededRuns - this.mathyRunCount;
        runs = Math.min(runs, this.maxRuns - this.mathyRunCount);
        if(runs>0) {
          this.lastTaskTime = this.elapsedTime;
          this.usedFrames++;
        }
        this.mathyRunCount+=runs;
        for(var i=0; i<runs && i<this.maxSimultaneousRuns; i++) {
          this.runs++;
          this.taskFun.runFun();
        }
      } else {
        this.taskFun.runFun();
        this.mathyRunCount++;
        this.runs++;
        this.usedFrames++;
        this.lastTaskTime = this.elapsedTime;
      }
    }
    if(this.elapsedTime>this.endTime || this.runs>=this.maxRuns || this.shouldTerminate()==true) this.terminate();
  },
  cancel: FS.Task.terminate,                          //Convenience.
  stop: FS.Task.terminate,                            //Convenience.
  reschedule: function(time) {                        //If you have a pointer to this Runner and want to postpone the startTime, you can. If it's already started, well, too late: you're outta luck!
    if(this.currentTime>=this.startTime) return false;
    this.elapsedTime = 0;
    this.startTime = time;
    return true;
  },
  status: function(asHTML, giveAlert) {               //Upgrades Task's status with some useful tidbits related to Runners.
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+="Start Time: "+this.startTime+"\n";
    s+="End Time: "+this.endTime+"\n";
    s+="Maximum Allowed Runs: "+this.maxRuns+"\n";
    s+="Frames with at least one run: "+this.usedFrames+"\n";
    s+="--------\n";
    s+="Runs: "+this.runs+"\n";
    s+="Interval: "+this.interval+"\n";
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  },
  toString: function() {
    var s = arguments.callee.base.call(this);
    var tminus = (this.startTime-this.elapsedTime);
    return s.substring(0,s.length-1)+" runs="+this.runs+((tminus>0)?(" start_in="+tminus):"")+"]";
  }
});
FS.setTimeout = function(task, delay, extras) {       //SUPER handy API that allows the use of FS.Runner without having to care about hashes and magic. Of course, you can customize with that "extras" hash if you want.
  var hash = {
    task: task,
    startTime: delay,
    maxRuns: 1
  };
  if(extras!=null) for(var i in extras) hash[i] = extras[i];
  return FS.Runner.create(hash).start();
};
FS.setInterval = function(task, interval, extras) {   //More handiness.
  var hash = {
    task: task,
    interval: interval
  };
  if(extras!=null) for(var i in extras) hash[i] = extras[i];
  return FS.Runner.create(hash).start();
};
FS.clearTimeout = function(task) {                    //As far as I can tell, set/clearTimeout and set/clearInterval complete the emulation of the builtin JS functionality, no?
  if(task) task.terminate();
};
FS.clearInterval = FS.clearTimeout;                   //You can use FS.Runner by just adding "FS." before all your timeouts and intervals. Should give performance a bit of a kick, methinks. Single run-loop forever!






















FS.Universe = FS.Object.extend({                      //Universe is just for data. Nothing does anything with it; it's so Particles can share their physics with one another.
  className: "Universe",
  gravity: function() {return {x:0, y:0};},
  wind: function() {return {x:0, y:0};},
  viscosity: function() {return 0;},
  density: function() {return 0;}
});

FS.Particle = FS.Task.extend({                            //By default, you get a regular ol' particle that will try to size itself to the element you pass it.
  className: "Particle",                                  //If it can't size itself from the element you gave it, it'll behave as if it's a 64px square.
  universe: null,   //will set in init if not supplied    //The default universe is a predefined earth-in-a-vacuum (FS.universes.earthVacuum).
  mass: 1,
  volume: 1,
  kineticFriction: function() {return 0;},                //Friction with the page. Separate from Universe because kinetic friction varies between different materials (and hence, particles).
  speedHalfLife: 0,                                       //Custom, non-physics-ish exponential speed degradation. Zero disables it. I had implemented this for creating particles out of thrown Draggables.
  bounceFactor: .99,                                      //Bounce factors of 1 or >0.95ish are dangerous... they get out of control because of the framerate and quantization of motion calculation. Bummer. Can fix, just low priority.
  constrainToWindow: false,
  minFPS: FS.maxFPS,

  element: null,    //supplied by user                    //REQUIRED! Without an element, it doesn't know how big to be! Perhaps I should make this not required, so you can have a mockup particle? Maybe.
  bounds: null,     //will set in init if not supplied    //Will be a FS.BoundedArea. This is the area where the particle can play. Defaults to the window's bounds, and default to constrain to those bounds. If bounds are supplied, then they are used and do not constrain to the window.
  p: null,          //will set in init if not supplied    //Position: after init, it'll be an instance of Mathster.Pair. Defaults to the center of the bounds.
  s: null,          //will set in init if not supplied    //Speed: after init, it'll be an instance of Mathster.Pair. Perhaps should be renamed to velocity? Defaults to (0,0)
  d: null,          //will set in init if not supplied    //Dimensions: after init, it'll be an instance of Mathster.Pair. Defaults to element's size if unsupplied, and (64,64) if element doesn't know how big it is.

  edgeMode: "die",                                        //Can be "bounce" or "wrap" or "die" or "persist" - Die causes the particle to terminate off the edge of the window. Persist makes the particle live on, even if offscreen. What about wrap? Remember a game called Maelstrom? Google it!

  bounceCount: 0,                                         //It keeps track of bounces.
  bounceFun: null,  //will set in init                    //Any function on this Particle with "onbounce" in the name will be put in this Fun Array. Can do some fun stuff with this! Trigger explosions... break things... blow stuff up...

  init: function() {                                      //Bunch of stuff I don't have time to cover right now. Namely, it inits bounds, p, s, and d, and tries to find correct/default values.
    arguments.callee.base.call(this);
    if(this.element==null) {
      this.element = makeBox();
    }
    if(this.bounds==null) {
      this.bounds = this.host.bounds.clone();
      this.constrainToWindow = true;
    }
    this.bounds.name = "Bounds for "+this.name;
    if(this.universe==null) this.universe = FS.universes.earthVacuum;

    this.arrangements = Framester.cssPositionRelativity(this.element);
    this.initP = Framester.getCSSPositionNumbers(this.element,this.arrangements);
    if(this.arrangements.h=="right") this.initP.x = this.bounds.X-this.initP.x;
    if(this.arrangements.v=="bottom") this.initP.y = this.bounds.Y-this.initP.y;

    if(this.p==null) this.p = FS.Pair.create({
      x:((this.arrangements.h=="both"||this.arrangements.h=="none")?this.bounds.c.x:this.initP.x),
      y:((this.arrangements.v=="both"||this.arrangements.v=="none")?this.bounds.c.y:this.initP.y)
    }); else if(!this.p.mag) this.p = FS.Pair.create({x:this.p.x, y:this.p.y});

    if(this.s==null) this.s = FS.Pair.create({
      x:2*Math.random()-1,
      y:2*Math.random()-1
    }); else if(!this.s.mag) this.s = FS.Pair.create({x:this.s.x, y:this.s.y});

    if(this.d==null) this.d = FS.Pair.create({
      x:this.element.offsetWidth?this.element.offsetWidth:64,
      y:this.element.offsetHeight?this.element.offsetHeight:64
    }); else if(!this.d.mag) this.d = FS.Pair.create({x:this.d.x, y:this.d.y});

    this.bounceFun = [].beFun(this);
    for(var i in this) {
      var it = this[i];
      if(i.toLowerCase().indexOf("onbounce")!=-1 && (typeof it=="function" || typeof it=="string")) this.bounceFun.addFun(it);
    }
    this.frameFun.addFun(this.render);

    if(this.constrainToWindow==true) {
      this.updateToWindow = this.updateToWindow.bind(this);
      this.host.resizeFun.addFun(this.updateToWindow);
    }
  },
  updateToWindow: function() {                            //Updates this Particle's bounds to match this host's bounds.
    this.bounds.setBounds({
      x: this.host.bounds.x,
      X: this.host.bounds.X,
      y: this.host.bounds.y,
      Y: this.host.bounds.Y
    });
  },
  run: function(time) {                                   //Fun physics, all hampered by the fact that it's calculated frame-by-frame instead of predefined with dynamically calculated functions of time! That's too big a project for this.
    if(this.running==false || this.paused==true || this.done==true) return;
    var g = this.universe.gravity(this.x,this.y);
    var wind = this.universe.wind(this.x,this.y);
    var visc = this.universe.viscosity(this.x,this.y);
    var d = this.universe.density(this.x,this.y);
    var kf = this.kineticFriction(this.x,this.y);

    var x = this.p.x+this.s.x*time;
    var y = this.p.y+this.s.y*time;

    var speedHypotenuse = this.s.mag();
    var kfx = speedHypotenuse>0?Math.abs(kf*this.s.x/speedHypotenuse):0;
    var kfy = speedHypotenuse>0?Math.abs(kf*this.s.y/speedHypotenuse):0;

    this.p.x = x;
    this.p.y = y;

    if(((this.p.x<=this.bounds.x && this.s.x<=0) || (this.p.x+this.d.x>=this.bounds.X && this.s.x>=0)) && this.edgeMode=="bounce") {
      this.s.x = this.bounceFactor*this.s.x*-1;
      this.s.y = this.bounceFactor*this.s.y;
      if(this.p.x+this.d.x>this.bounds.X) this.p.x = this.bounds.X-this.d.x;
      if(this.p.x<this.bounds.x)          this.p.x = this.bounds.x;
      this.bounceCount++;
      this.bounceFun.runFun();
    } else {
      this.s.x += g.x*time/1000;
      var subtraction = Math.abs(kfx+visc*this.volume/this.mass*(this.s.x+(this.s.x-wind.x))*(this.s.x+(this.s.x-wind.x))*time/1000);
      var wasMoreThanWind = false;
      if(this.s.x>wind.x) {
        wasMoreThanWind = true;
        this.s.x-=subtraction;
      }
      else this.s.x+=subtraction;
      if(wasMoreThanWind && this.s.x<wind.x || !wasMoreThanWind && this.s.x>wind.x) this.s.x = wind.x;
      if(this.speedHalfLife>0) this.s.x*=Math.pow(0.5,time/this.speedHalfLife);
    }

    if(((this.p.y<=this.bounds.y && this.s.y<=0) || (this.p.y+this.d.y>=this.bounds.Y && this.s.y>=0)) && this.edgeMode=="bounce") {
      this.s.x = this.bounceFactor*this.s.x;
      this.s.y = this.bounceFactor*this.s.y*-1;
      if(this.p.y+this.d.y>this.bounds.Y) this.p.y = this.bounds.Y-this.d.y;
      if(this.p.y<this.bounds.y)          this.p.y = this.bounds.y;
      this.bounceCount++;
      this.bounceFun.runFun();
    } else {
      this.s.y += g.y*time/1000;
      var subtraction = Math.abs(kfy+visc*this.volume/this.mass*(this.s.y+(this.s.y-wind.y))*(this.s.y+(this.s.y-wind.y))*time/1000);
      var wasMoreThanWind = false;
      if(this.s.y>wind.y) {
        wasMoreThanWind = true;
        this.s.y-=subtraction;
      }
      else this.s.y+=subtraction;
      if(wasMoreThanWind && this.s.y<wind.y || !wasMoreThanWind && this.s.y>wind.y) this.s.y = wind.y;
      if(this.speedHalfLife>0) this.s.y*=Math.pow(1/2,time/this.speedHalfLife);
    }
    this.terminateIfOutOfBounds();
  },
  render: function() {                                    //This does nothing but position the element. Init automagically adds this function to this Particle's frameFun pile.
    if(this.edgeMode=="wrap") {
      this.element.style.left = (this.p.x+100000000*this.bounds.X)%this.bounds.X+"px";
      this.element.style.top = (this.p.y+100000000*this.bounds.Y)%this.bounds.Y+"px";
      return;
    }
    this.element.style.left = this.p.x+"px";
    this.element.style.top = this.p.y+"px";
  },
  terminateIfOutOfBounds: function() {                    //Only effective if edgeMode is "die" - If the particle is far enough offscreen, it removes the element and terminates the particle.
    if(this.edgeMode=="persist" || this.edgeMode=="wrap") return;
    var leewayX, leewayY;
    if(this.element.offsetWidth==undefined || this.element.offsetHeight==undefined || this.element.offsetWidth==null || this.element.offsetHeight==null) {
      leewayX = Framester.window.w;
      leewayY = Framester.window.h;
    } else {
      leewayX = this.element.offsetWidth;
      leewayY = this.element.offsetHeight;
    }//console.log('attempting to terminate' + this.p.x+"m"+(this.bounds.X+leewayX)+" || "+this.p.x+"m"+(this.bounds.x-leewayX)+" || "+this.p.y+"m"+(this.bounds.Y+leewayY)+" || "+this.p.y+"m"+(this.bounds.y-leewayY));
    if(this.p.x>this.bounds.X+leewayX || this.p.x<this.bounds.x-leewayX || this.p.y>this.bounds.Y+leewayY || this.p.y<this.bounds.y-leewayY) {
      if(this.element.parentNode!=null) this.element.parentNode.removeChild(this.element);
      this.terminate();
    }
  },
  terminate: function() {
    arguments.callee.base.call(this);
    this.host.resizeFun.removeFun(this.updateToWindow);
  },
  status: function(asHTML, giveAlert) {                   //Upgrades Task's status with some useful tidbits related to Particles.
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+="Universe: "+this.universe+"\n";
    s+="Mass: "+this.mass+"\n";
    s+="Volume: "+this.volume+"\n";
    s+="Speed Halflife: "+this.speedHalfLife+"\n";
    s+="Edge Mode: "+this.edgeMode+"\n";
    s+="--------\n";
    s+="Bounce Count: "+this.bounceCount+"\n";
    s+="--------\n";
    s+="Bounds: "+this.bounds+"\n";
    s+="--------\n";
    s+="Position: "+this.p.toTinyString()+"\n";
    s+="Speed: "+this.s.toTinyString()+"\n";
    s+="Dimensions: "+this.d.toTinyString()+"\n";
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  },
  toString: function() {
    var s = arguments.callee.base.call(this);
    s = s.substring(0,s.length-1);
    return s+" p="+this.p.toTinyString()+" s="+this.s.toTinyString()+" d="+this.d.toTinyString()+" bounds="+this.bounds.toTinyString()+"]";
  }
});

FS.universes = FS.Object.create({                         //Some predefined physics sets! Expect this list to grow dramatically! You can say "universe: FS.universes.shampoo," in your hash to create a particle. w00t!
  name: "Framester Stock Universe Bank",
  earthVacuum: FS.Universe.create({
    name: "Earth Gravity in a Vacuum",
    gravity: function() {return {x:0, y:1};}
  }),
  air: FS.Universe.create({
    name: "Earth Gravity in Air",
    gravity: function() {return {x:0, y:1};},
    viscosity: function() {return .3;}
  }),
  shampoo: FS.Universe.create({
    name: "Earth Gravity in Shampoo",
    gravity: function() {return {x:0, y:1};},
    viscosity: function() {return 1.5;}
  }),
  windyAir: FS.Universe.create({
    name: "Earth Gravity in Windy Air",
    gravity: function() {return {x:0, y:1};},
    viscosity: function() {return .3;},
    wind: function() {return {x:1,y:0};}
  })
});




























FS.Draggable = FS.Task.extend({
  className: "Draggable",
  minFPS: FS.minFPS,
  element: null,                //required
  p: null,                      //for init (if unsupplied, find values or use defaults)
  d: null,                      //for init (if unsupplied, find values or leave NULL!)
  bounds: null,                 //for init (if unsupplied, use and bind to window bounds. If supplied, use supplied and do not bind to window bounds.)
  mouseIsDown: false,
  dragging: false,
  minDragRadius: 1,             //Will not initiate drag unless this many pixels have been covered. Otherwise, onmouseup is a click.
  validate: function() {return true;},    //Aborts everything starting from mousedown if this function returns false. Customize it!
  downFun: null,                //for init
  dragFun: null,                //for init
  dropFun: null,                //for init
  clickFun: null,               //for init

  init: function() {
    arguments.callee.base.call(this);
    if(this.element==null) {
      this.element = makeBox();
    }
    if(this.element.dragger!=null) this.element.dragger.terminate();
    if(this.bounds==null) {
      this.bounds = this.host.bounds.clone();
      this.constrainToWindow = true;
    } else if(!this.bounds._isFS) this.bounds = FS.BoundedArea.create(this.bounds);
    this.bounds.name = "Bounds for "+this.name;

    this.arrangements = Framester.cssPositionRelativity(this.element);
    this.initP = Framester.getCSSPositionNumbers(this.element,this.arrangements);
    if(this.arrangements.h=="right") this.initP.x = this.bounds.X-this.initP.x;
    if(this.arrangements.v=="bottom") this.initP.y = this.bounds.Y-this.initP.y;

    if(this.p==null) this.p = FS.Pair.create({
      x:((this.arrangements.h=="both"||this.arrangements.h=="none")?this.bounds.c.x:this.initP.x),
      y:((this.arrangements.v=="both"||this.arrangements.v=="none")?this.bounds.c.y:this.initP.y)
    }); else if(!this.p.mag) this.p = FS.Pair.create({x:this.p.x, y:this.p.y});

    this.lastP = FS.Pair.create();

    if(this.d==null) {
      this.d = FS.Pair.create({
        x:this.element.offsetWidth?this.element.offsetWidth:0,
        y:this.element.offsetHeight?this.element.offsetHeight:0
      });
      this.constrainToDimensions = (this.element.offsetWidth!=undefined && this.element.offsetHeight!=undefined && this.element.offsetWidth!=null && this.element.offsetHeight!=null);
    }
    else if(!this.d.mag) {
      this.d = FS.Pair.create({x:this.d.x, y:this.d.y});
      this.constrainToDimensions = false;
    }

    this.downFun = [].beFun(this);
    this.dragFun = [].beFun(this);
    this.dropFun = [].beFun(this);
    this.clickFun = [].beFun(this);

    for(var i in this) {
      var it = this[i];
      if(i.toLowerCase().indexOf("ondown")!=-1 && (typeof it=="function" || typeof it=="string")) this.downFun.addFun(it);
      if(i.toLowerCase().indexOf("ondrag")!=-1 && (typeof it=="function" || typeof it=="string")) this.dragFun.addFun(it);
      if(i.toLowerCase().indexOf("ondrop")!=-1 && (typeof it=="function" || typeof it=="string")) this.dropFun.addFun(it);
      if(i.toLowerCase().indexOf("onclick")!=-1 && (typeof it=="function" || typeof it=="string")) this.clickFun.addFun(it);
    }

    this.mouseMoved = this.mouseMoved.bind(this);
    this.mouseClickedDown = this.mouseClickedDown.bind(this);
    this.mouseClickedUp = this.mouseClickedUp.bind(this);

    this.element.onmousedown = this.mouseClickedDown;
    this.element.dragger = this;
    this.element.ondrag = function() {return false;};
    this.element.onmousemove = function() {return false;};
    this.element.onselectstart = function() {return false;};

    if(this.render && typeof this.render=="function") this.dragFun.addFun(this.render);
    if(this.constrainToWindow==true) {
      this.updateToWindow = this.updateToWindow.bind(this);
      this.host.resizeFun.addFun(this.updateToWindow);
      this.finishFun.addFun("FSHost.resizeFun.removeFun(this.updateToWindow);");
    }

    if(this.constrainToDimensions==true) {
      this.updateToDimensions = this.updateToDimensions.bind(this);
      this.frameFun.addFun(this.updateToDimensions);
      this.frameFun.addFun(this.boundCheck);
    }

    // if(this.allowStatusBox) this.element.ondblclick = this._setUpStatusBox.bind(this);
  },

  updateToWindow: function() {                  //Updates this Draggable's bounds to match this host's bounds.
    this.bounds.setBounds({
      x: this.host.bounds.x,
      X: this.host.bounds.X,
      y: this.host.bounds.y,
      Y: this.host.bounds.Y
    });
  },

  updateToDimensions: function() {              //Updates this Draggable's dimensions to match the dimensions of its element.
    this.d.mixin({
      x:this.element.offsetWidth?this.element.offsetWidth:0,
      y:this.element.offsetHeight?this.element.offsetHeight:0
    });
  },

  render: function() {
    if(this.element.style.right!="")  this.element.style.right="";
    if(this.element.style.bottom!="") this.element.style.bottom="";
    if(this.element.style.left!=this.p.x+"px") this.element.style.left = this.p.x+"px";
    if(this.element.style.top !=this.p.y+"px") this.element.style.top  = this.p.y+"px";
  },

  mouseClickedDown: function(e) {
    if(!this.validate()) return;
    if(!this.downInitP) this.downInitP = FS.Pair.create();
    this.downInitP.x = this.p.x;
    this.downInitP.y = this.p.y;
    if(!this.downInitMouseP) this.downInitMouseP = FS.Pair.create();
    this.downInitMouseP.x = this.host.mouse.x;
    this.downInitMouseP.y = this.host.mouse.y;
    this.mouseIsDown = true;
    this.dragging = false;
    this.minFPS = FS.maxFPS;
    this.host.mouseMoveFun.addFun(this.mouseMoved);
    this.host.mouseUpFun.addFun(this.mouseClickedUp);
    this.downFun.runFun(e);
  },

  mouseClickedUp: function(e) {
    this.host.mouseMoveFun.removeFun(this.mouseMoved);
    this.host.mouseUpFun.removeFun(this.mouseClickedUp);
    this.mouseIsDown = false;
    var wasDragging = this.dragging;
    this.dragging = false;
    this.minFPS = FS.minFPS;
    if(wasDragging) this.dropFun.runFun(e);
    else this.clickFun.runFun(e);
  },

  mouseMoved: function(e) {
    this.p.x = this.downInitP.x+this.host.mouse.x-this.downInitMouseP.x;
    this.p.y = this.downInitP.y+this.host.mouse.y-this.downInitMouseP.y;
    this.boundCheck();
    if(this.p.distanceFrom(this.downInitP)>=this.minDragRadius) {
      this.dragging = true;
    }
  },

  boundCheck: function(e) {
    var oldx = this.p.x, oldy = this.p.y;
    this.p.x = FS.bound(this.p.x, this.bounds.x, this.bounds.X-this.d.x);
    this.p.y = FS.bound(this.p.y, this.bounds.y, this.bounds.Y-this.d.y);
    if(this.p.x!=oldx || this.p.y!=oldy)
      if(this.render) this.render();
  },

  onFrameForWhileMouseIsDown: function(e) {
    if(!this.mouseIsDown || !this.dragging) return;
    if(this.lastP.x!=this.p.x || this.lastP.y!=this.p.y)
      this.dragFun.runFun(e);
    this.lastP.x = this.p.x;
    this.lastP.y = this.p.y;
  },

  status: function(asHTML, giveAlert) {
    var s = arguments.callee.base.call(this);
    s+="--------\n";
    s+="Element: "+this.element+"\n";
    s+="Position: "+this.p.toTinyString()+"\n";
    s+="Dimensions: "+this.d.toTinyString()+"\n";
    s+="Mouse Down: "+this.mouseIsDown+"\n";
    s+="Dragging: "+this.dragging+"\n";
    if(this.mouseIsDown) {
      s+="Previous Frame's Pos: "+this.lastP.toTinyString()+"\n";
      s+="Initial Down Pos: "+this.downInitP.toTinyString()+"\n";
      s+="Initial Down Mouse Pos: "+this.downInitMouseP.toTinyString()+"\n";
    }
    if(giveAlert) alert(s);
    return asHTML?s.asHTML():s;
  },

  toString: function() {
    return arguments.callee.base.call(this);
  }
});



















FS.Signal = FS.Task.extend({
  className: "Signal",
  c: 0,
  init: function() {
    arguments.callee.base.call(this);
    this.c = this.f(0);
  },
  f: function(time) {
    return 0;
  },
  updateCurrentValueOnFrame: function() {
    this.c = this.f(this.elapsedTime);
  }
});
FS.zeroSignal = FS.Signal.create();
                                   

FS.Signal.Sinusoid = FS.Signal.extend({
  className: "Signal.Sinusoid",
  a: 1,
  phase: 0,
  w: 2*Math.PI,
  f: function(t) {
    return this.a*Math.cos(this.w*t/1000+this.phase);
  }
});






FS.Filter = FS.Signal.extend({
  className: "Filter",
  signal: FS.zeroSignal,
  f: function() {
    return this.signal.c;
  }
});

FS.Filter.LowPass = FS.Filter.extend({
  className: "Filter.LowPass",
  alpha: .99,
  f: function() {
    return this.c*this.alpha + this.signal.c*(1-this.alpha);
  }
});

FS.Filter.HighPass = FS.Filter.extend({
  className: "Filter.HighPass",
  alpha: .99,
  f: function() {
    var ret;
    if(this.lastSigC===undefined)
      ret = this.signal.c;
    else 
      ret = this.alpha*(this.c + this.signal.c - this.lastSigC);
    this.lastSigC = this.signal.c;
    return ret;
  }
});



















/******Presupplied quick functions******/
FS.Fader = FS.Animation.extend({
  className: "Fader",
  element: null,
  render: function() {
    if(!this.element) {
      this.terminate();
      return;
    }
    FS.setOpacity(this.element,this.f[0].c);
    if(this.f[0].c==0 && !this.visibleAtZero && this.element.style.visibility!="hidden") this.element.style.visibility = "hidden";
    if(this.f[0].c!=0 && !this.visibleAtZero && this.element.style.visibility!="visible") this.element.style.visibility = "visible";
  }
});

FS.fade = function(element, tO, sp, a, hash) {
  if(!a) a=0;
  if(!sp) sp=1;
  if(!tO) tO=0;
  var initialOpacity = FS.getOpacity(element);
  var arguments = {element: element, f:{t:tO, c:initialOpacity, sp:sp, a:a, s:initialOpacity}};
  if(hash) for(var i in hash) arguments[i] = hash[i];
  var fader = FS.Fader.create(arguments).start();
  if(element.FSFader && element.FSFader.terminate) element.FSFader.terminate();
  element.FSFader = fader;
  return fader;
};




FS.init();




var initT2 = Math.floor(new Date());          //This benches all the Class setup code.




var colors = ["#FF0000","#000088","#00FF00","#000000","#333333","#440000","#004444","#FF8800","#88FF00","#FF0088","#8800FF","#0088FF","#00FF88"];
var color = 0;
function getColor() {
  return colors[(color++)%colors.length];
}
function makeBox(host) {                      //Handy. ;-)
  if(!host) host = FSHost;
  window.box = document.createElement('div');
  // box.innerHTML = "";
  box.style.cssText = "position:absolute; left: "+(host.bounds.c.x-30)+"px; top:"+(host.bounds.c.y-30)+"px; z-index:100000; width:60px; height:60px; opacity:0.5; background:"+getColor()+";";
  host.document.body.appendChild(box);
  return box;
}