Initial commit
authordsc <david.schoonover@gmail.com>
Sat, 30 Oct 2010 23:54:53 +0000 (16:54 -0700)
committerdsc <david.schoonover@gmail.com>
Sat, 30 Oct 2010 23:54:53 +0000 (16:54 -0700)
98 files changed:
css/log.css [new file with mode: 0644]
css/lttl.css [new file with mode: 0644]
css/reset.css [new file with mode: 0644]
css/test.css [new file with mode: 0644]
index.php [new file with mode: 0644]
lib/cake.js [new file with mode: 0644]
lib/excanvas.js [new file with mode: 0644]
lib/excanvas.min.js [new file with mode: 0644]
lib/functional.js [new file with mode: 0644]
lib/gury.js [new file with mode: 0644]
lib/jquery-1.4.3.js [new file with mode: 0644]
lib/jquery-1.4.3.min.js [new file with mode: 0644]
lib/jquery.hotkeys.js [new file with mode: 0644]
lib/jquery.hotkeys.min.js [new file with mode: 0644]
lib/processing-0.9.7.js [new file with mode: 0644]
lib/processing-0.9.7.min.js [new file with mode: 0644]
lib/qunit/qunit.css [new file with mode: 0644]
lib/qunit/qunit.js [new file with mode: 0644]
lib/raphael-min.js [new file with mode: 0644]
lib/raphael.js [new file with mode: 0644]
lib/uki-0.3.8.js [new file with mode: 0644]
lib/uki-0.3.8.min.js [new file with mode: 0644]
lib/uki-more-0.3.8.min.js [new file with mode: 0644]
notes.md [new file with mode: 0644]
src/Y/_intro.js [new file with mode: 0644]
src/Y/_outro.js [new file with mode: 0644]
src/Y/alias.js [new file with mode: 0644]
src/Y/core.js [new file with mode: 0644]
src/Y/modules/event.js [new file with mode: 0644]
src/Y/modules/metaclass.js [new file with mode: 0644]
src/Y/modules/y.event.js [new file with mode: 0644]
src/Y/modules/y.json.js [new file with mode: 0644]
src/Y/modules/y.kv.js [new file with mode: 0644]
src/Y/modules/y.op.js [new file with mode: 0644]
src/Y/modules/y.plugin-arch.js [new file with mode: 0644]
src/Y/modules/y.polyevent.js [new file with mode: 0644]
src/Y/type.js [new file with mode: 0644]
src/Y/y-array.js [new file with mode: 0644]
src/Y/y-class.js [new file with mode: 0644]
src/Y/y-collection.js [new file with mode: 0644]
src/Y/y-core.js [new file with mode: 0644]
src/Y/y-function.js [new file with mode: 0644]
src/Y/y-number.js [new file with mode: 0644]
src/Y/y-object.js [new file with mode: 0644]
src/Y/y-op.js [new file with mode: 0644]
src/Y/y-string.js [new file with mode: 0644]
src/Y/y.js.php [new file with mode: 0644]
src/js/_intro.js [new file with mode: 0644]
src/js/_outro.js [new file with mode: 0644]
src/js/array.js [new file with mode: 0644]
src/js/core.js [new file with mode: 0644]
src/js/function.js [new file with mode: 0644]
src/js/js.js.php [new file with mode: 0644]
src/js/number.js [new file with mode: 0644]
src/js/object.js [new file with mode: 0644]
src/js/regexp.js [new file with mode: 0644]
src/js/string.js [new file with mode: 0644]
src/js/type.js [new file with mode: 0644]
src/lessly/bitgrid.js [new file with mode: 0644]
src/lessly/draw.js [new file with mode: 0644]
src/lessly/future.js [new file with mode: 0644]
src/lessly/log.js [new file with mode: 0644]
src/lessly/log.uki.js [new file with mode: 0644]
src/lessly/viewport.js [new file with mode: 0644]
src/portal/layer.js [new file with mode: 0644]
src/portal/path.js [new file with mode: 0644]
src/portal/portal.js [new file with mode: 0644]
src/portal/shape.js [new file with mode: 0644]
src/portal/simpleclass.js [new file with mode: 0644]
src/portal/util/cooldown.js [new file with mode: 0644]
src/portal/util/eventloop.js [new file with mode: 0644]
src/portal/util/loc.js [new file with mode: 0644]
src/portal/util/pointquadtree.js [new file with mode: 0644]
src/portal/util/quadtree.js [new file with mode: 0644]
src/portal/util/rbtree.js [new file with mode: 0644]
src/simoon/ability/ability.js [new file with mode: 0644]
src/simoon/ability/laser.js [new file with mode: 0644]
src/simoon/ability/projectile.js [new file with mode: 0644]
src/simoon/game/calc.js [new file with mode: 0644]
src/simoon/game/draw.js [new file with mode: 0644]
src/simoon/game/game.js [new file with mode: 0644]
src/simoon/game/map.js [new file with mode: 0644]
src/simoon/globals.js [new file with mode: 0644]
src/simoon/grid/grid.js [new file with mode: 0644]
src/simoon/player/player.js [new file with mode: 0644]
src/simoon/player/test-player.js [new file with mode: 0644]
src/simoon/simoon.js [new file with mode: 0644]
src/simoon/ui.js [new file with mode: 0644]
src/simoon/unit/agent.js [new file with mode: 0644]
src/simoon/unit/creep.js [new file with mode: 0644]
src/simoon/unit/tower.js [new file with mode: 0644]
src/simoon/unit/unit.js [new file with mode: 0644]
src/tanks/game.js [new file with mode: 0644]
src/tanks/lttl.js [new file with mode: 0644]
src/tanks/map.js [new file with mode: 0644]
src/tanks/tank.js [new file with mode: 0644]
src/tanks/ui.js [new file with mode: 0644]
src/u.js [new file with mode: 0644]

diff --git a/css/log.css b/css/log.css
new file mode 100644 (file)
index 0000000..897fee1
--- /dev/null
@@ -0,0 +1,12 @@
+#log {
+       position:fixed; top:0; right:0; width:50%; height:100%; overflow:hidden;
+       border-left:1px solid #bbb; background-color: #fff; color:#333; }
+#log h5 { font-size:1.25em; font-weight:normal; text-transform:uppercase; letter-spacing:0.16667em; }
+
+.log_meta { position:absolute; top:0; left:0; width:100%; z-index:2; background-color:#fff; }
+.log_meta > * { padding:0.3em; }
+       .log_menu { color:#fff; background-color:#bbb; }
+       .log_menu a { color:#fff; cursor:pointer; }
+.log_container { position:relative; width:100%; height:100%; overflow:auto; z-index:1; }
+       .log_item { padding:0.5em; font:normal normal 10pt/1 Menlo, Monospace; text-decoration:none; }
+
diff --git a/css/lttl.css b/css/lttl.css
new file mode 100644 (file)
index 0000000..d746829
--- /dev/null
@@ -0,0 +1,18 @@
+html, body { width:100%; height:100%; 
+    font-family:Geogrotesque,Helvetica; color:#fff; background-color:#3F3F3F; }
+body { font-family:Geogrotesque, Helvetica; font-size:12pt; }
+h1 { position:fixed; top:0; right:0; margin:0; padding:0; font-size:3em; color:#000; opacity:0.25; z-index:100; }
+ul, ol, li { list-style: none ! important; margin:0; padding:0; }
+
+.rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; }
+
+#viewport { position:relative; top:1em; width:500px; height:500px; margin:0 auto; }
+
+#howto { position:fixed; top:3em; right:1em; color:#BFBFBF; }
+
+#info { position:fixed; bottom:10px; right:10px; padding:0.5em; background-color:rgba(0,0,0, 0.1); color:#787878; }
+    #info label { display:block; float:left; width:3em; margin-right:0.5em; color:#787878; }
+    #info input { border:0; background-color:transparent; min-width:5em; width:5em; color:#5c5c5c; }
+
+#log { position:fixed; top:auto; bottom:0; left:0; width:100%; height:30%; border-top:1px solid #bbb; }
+
diff --git a/css/reset.css b/css/reset.css
new file mode 100644 (file)
index 0000000..dd5886b
--- /dev/null
@@ -0,0 +1,2 @@
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;font-variant:normal;}sup {vertical-align:text-top;}sub {vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}
+h1{font-size:138.5%;}h2{font-size:123.1%;}h3{font-size:108%;}h1,h2,h3{margin:1em 0;}h1,h2,h3,h4,h5,h6,strong{font-weight:bold;}abbr,acronym{border-bottom:1px dotted #000;cursor:help;} em{font-style:italic;}blockquote,ul,ol,dl{margin:1em;}ol,ul,dl{margin-left:2em;}ol li{list-style:decimal outside;}ul li{list-style:disc outside;}dl dd{margin-left:1em;}th,td{padding:.5em;}th{font-weight:bold;text-align:center;}caption{margin-bottom:.5em;text-align:center;}p,fieldset,table,pre{margin-bottom:1em;}input[type=text],input[type=password],textarea{width:12.25em;*width:11.9em;}
\ No newline at end of file
diff --git a/css/test.css b/css/test.css
new file mode 100644 (file)
index 0000000..1c2d3e4
--- /dev/null
@@ -0,0 +1,4 @@
+html, body { width:100%; height:100%; 
+    font-family:Geogrotesque,Helvetica; color:#fff; background-color:#3F3F3F; }
+h1:not([id]) { position:fixed; top:-0.25em; right:-0.25em; margin:0; padding:0;
+    font-size:3em; color:#000; opacity:0.25; z-index:100; }
diff --git a/index.php b/index.php
new file mode 100644 (file)
index 0000000..8806461
--- /dev/null
+++ b/index.php
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>The Littlest Battletank</title>
+<link rel="stylesheet" href="css/reset.css" type="text/css" media="screen">
+<link rel="stylesheet" href="css/lttl.css" type="text/css" media="screen">
+</head>
+<body class="lttl tanks">
+
+
+<div id="viewport"></div>
+
+<h1>The Littlest Battletank</h1>
+
+<ul id="info" class="rounded">
+    <li id="state"></li>
+    <li><label for="fps">fps</label> <input id="fps" name="fps" value="" type="text"></li>
+    <li><label for="frame">frame</label> <input id="frame" name="frame" value="" type="text"></li>
+    <li><label for="agents">agents</label> <input id="agents" name="agents" value="" type="text"></li>
+    <li><label for="units">units</label> <input id="units" name="units" value="" type="text"></li>
+    <li><label for="bullets">bullets</label> <input id="bullets" name="bullets" value="" type="text"></li>
+</ul>
+
+<div id="log" style="display:none"></div>
+
+<div id="scripts">
+    <!--[if IE]><script type="text/javascript" src="lib/excanvas.min.js"></script><![endif]-->
+<?php
+$scripts = array(
+    "lib/jquery-1.4.3.js",
+    "lib/jquery.hotkeys.js",
+    
+    // "http://static.ukijs.org/pkg/0.3.8/uki.js",
+    // "http://static.ukijs.org/pkg/0.3.8/uki-more.js",
+    
+    "src/lessly/future.js",
+    
+    "src/Y/y.js.php",
+    "src/Y/modules/y.event.js",
+    
+    "src/portal/layer.js",
+    "src/portal/shape.js",
+    
+    // "src/portal/util/quadtree.js",
+    // "src/portal/util/rbtree.js",
+    // "src/portal/util/eventloop.js",
+    // "src/portal/util/cooldown.js",
+    // "src/portal/util/loc.js",
+    
+    "src/tanks/map.js",
+    "src/tanks/tank.js",
+    "src/tanks/game.js",
+    "src/tanks/ui.js",
+    
+    "src/tanks/lttl.js"
+);
+
+function js($src) {
+    echo "    <script src=\"$src\" type=\"text/javascript\"></script>\n";
+}
+
+foreach ($scripts as $s) js($s);
+?>
+</div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/lib/cake.js b/lib/cake.js
new file mode 100644 (file)
index 0000000..e222e3c
--- /dev/null
@@ -0,0 +1,7657 @@
+/*
+CAKE - Canvas Animation Kit Experiment
+
+Copyright (C) 2007  Ilmari Heikkinen
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+
+/**
+  Delete the first instance of obj from the array.
+
+  @param obj The object to delete
+  @return true on success, false if array contains no instances of obj
+  @type boolean
+  @addon
+  */
+Array.prototype.deleteFirst = function(obj) {
+  for (var i=0; i<this.length; i++) {
+    if (this[i] == obj) {
+      this.splice(i,1)
+      return true
+    }
+  }
+  return false
+}
+
+Array.prototype.stableSort = function(cmp) {
+  // hack to work around Chrome's qsort
+  for(var i=0; i<this.length; i++) {
+    this[i].__arrayPos = i;
+  }
+  return this.sort(Array.__stableSorter(cmp));
+}
+Array.__stableSorter = function(cmp) {
+  return (function(c1, c2) {
+    var r = cmp(c1,c2);
+    if (!r) { // hack to work around Chrome's qsort
+      return c1.__arrayPos - c2.__arrayPos
+    }
+    return r;
+  });
+}
+
+/**
+  Compares two arrays for equality. Returns true if the arrays are equal.
+  */
+Array.prototype.equals = function(array) {
+  if (!array) return false
+  if (this.length != array.length) return false
+  for (var i=0; i<this.length; i++) {
+    var a = this[i]
+    var b = array[i]
+    if (a.equals && typeof(a.equals) == 'function') {
+      if (!a.equals(b)) return false
+    } else if (a != b) {
+      return false
+    }
+  }
+  return true
+}
+
+/**
+  Rotates the first element of an array to be the last element.
+  Rotates last element to be the first element when backToFront is true.
+
+  @param {boolean} backToFront Whether to move the last element to the front or not
+  @return The last element when backToFront is false, the first element when backToFront is true
+  @addon
+  */
+Array.prototype.rotate = function(backToFront) {
+  if (backToFront) {
+    this.unshift(this.pop())
+    return this[0]
+  } else {
+    this.push(this.shift())
+    return this[this.length-1]
+  }
+}
+/**
+  Returns a random element from the array.
+
+  @return A random element
+  @addon
+ */
+Array.prototype.pick = function() {
+  return this[Math.floor(Math.random()*this.length)]
+}
+
+Array.prototype.flatten = function() {
+  var a = []
+  for (var i=0; i<this.length; i++) {
+    var e = this[i]
+    if (e.flatten) {
+      var ef = e.flatten()
+      for (var j=0; j<ef.length; j++) {
+        a[a.length] = ef[j]
+      }
+    } else {
+      a[a.length] = e
+    }
+  }
+  return a
+}
+
+Array.prototype.take = function() {
+  var a = []
+  for (var i=0; i<this.length; i++) {
+    var e = []
+    for (var j=0; j<arguments.length; j++) {
+      e[j] = this[i][arguments[j]]
+    }
+    a[i] = e
+  }
+  return a
+}
+
+if (!Array.prototype.pluck) {
+  Array.prototype.pluck = function(key) {
+    var a = []
+    for (var i=0; i<this.length; i++) {
+      a[i] = this[i][key]
+    }
+    return a
+  }
+}
+
+Array.prototype.set = function(key, value) {
+  for (var i=0; i<this.length; i++) {
+    this[i][key] = value
+  }
+}
+
+Array.prototype.allWith = function() {
+  var a = []
+  topLoop:
+  for (var i=0; i<this.length; i++) {
+    var e = this[i]
+    for (var j=0; j<arguments.length; j++) {
+      if (!this[i][arguments[j]])
+        continue topLoop
+    }
+    a[a.length] = e
+  }
+  return a
+}
+
+// some common helper methods
+
+if (!Function.prototype.bind) {
+  /**
+    Creates a function that calls this function in the scope of the given
+    object.
+
+      var obj = { x: 'obj' }
+      var f = function() { return this.x }
+      window.x = 'window'
+      f()
+      // => 'window'
+      var g = f.bind(obj)
+      g()
+      // => 'obj'
+
+    @param object Object to bind this function to
+    @return Function bound to object
+    @addon
+    */
+  Function.prototype.bind = function(object) {
+    var t = this
+    return function() {
+      return t.apply(object, arguments)
+    }
+  }
+}
+
+if (!Array.prototype.last) {
+  /**
+    Returns the last element of the array.
+
+    @return The last element of the array
+    @addon
+    */
+  Array.prototype.last = function() {
+    return this[this.length-1]
+  }
+}
+if (!Array.prototype.indexOf) {
+  /**
+    Returns the index of obj if it is in the array.
+    Returns -1 otherwise.
+
+    @param obj The object to find from the array.
+    @return The index of obj or -1 if obj isn't in the array.
+    @addon
+    */
+  Array.prototype.indexOf = function(obj) {
+    for (var i=0; i<this.length; i++)
+      if (obj == this[i]) return i
+    return -1
+  }
+}
+if (!Array.prototype.includes) {
+  /**
+    Returns true if obj is in the array.
+    Returns false if it isn't.
+
+    @param obj The object to find from the array.
+    @return True if obj is in the array, false if it isn't
+    @addon
+    */
+  Array.prototype.includes = function(obj) {
+    return (this.indexOf(obj) >= 0);
+  }
+}
+/**
+  Iterate function f over each element of the array and return an array
+  of the return values.
+
+  @param f Function to apply to each element
+  @return An array of return values from applying f on each element of the array
+  @type Array
+  @addon
+  */
+Array.prototype.map = function(f) {
+  var na = new Array(this.length)
+  if (f)
+    for (var i=0; i<this.length; i++) na[i] = f(this[i], i, this)
+  else
+    for (var i=0; i<this.length; i++) na[i] = this[i]
+  return na
+}
+Array.prototype.forEach = function(f) {
+  for (var i=0; i<this.length; i++) f(this[i], i, this)
+}
+if (!Array.prototype.reduce) {
+  Array.prototype.reduce = function(f, s) {
+    var i = 0
+    if (arguments.length == 1) {
+      s = this[0]
+      i++
+    }
+    for(; i<this.length; i++) {
+      s = f(s, this[i], i, this)
+    }
+    return s
+  }
+}
+if (!Array.prototype.find) {
+  Array.prototype.find = function(f) {
+    for(var i=0; i<this.length; i++) {
+      if (f(this[i], i, this)) return this[i]
+    }
+  }
+}
+
+if (!String.prototype.capitalize) {
+  /**
+    Returns a copy of this string with the first character uppercased.
+
+    @return Capitalized version of the string
+    @type String
+    @addon
+    */
+  String.prototype.capitalize = function() {
+    return this.replace(/^./, this.slice(0,1).toUpperCase())
+  }
+}
+
+if (!String.prototype.escape) {
+  /**
+    Returns a version of the string that can be used as a string literal.
+
+    @return Copy of string enclosed in double-quotes, with double-quotes
+            inside string escaped.
+    @type String
+    @addon
+    */
+  String.prototype.escape = function() {
+    return '"' + this.replace(/"/g, '\\"') + '"'
+  }
+}
+if (!String.prototype.splice) {
+  String.prototype.splice = function(start, count, replacement) {
+    return this.slice(0,start) + replacement + this.slice(start+count)
+  }
+}
+if (!String.prototype.strip) {
+  /**
+    Returns a copy of the string with preceding and trailing whitespace
+    removed.
+
+    @return Copy of string sans surrounding whitespace.
+    @type String
+    @addon
+    */
+  String.prototype.strip = function() {
+    return this.replace(/^\s+|\s+$/g, '')
+  }
+}
+
+if (!window['$A']) {
+  /**
+    Creates a new array from an object with #length.
+    */
+  $A = function(obj) {
+    var a = new Array(obj.length)
+    for (var i=0; i<obj.length; i++)
+      a[i] = obj[i]
+    return a
+  }
+}
+
+if (!window['$']) {
+  $ = function(id) {
+    return document.getElementById(id)
+  }
+}
+
+if (!Math.sinh) {
+  /**
+    Returns the hyperbolic sine of x.
+
+    @param x The value for x
+    @return The hyperbolic sine of x
+    @addon
+    */
+  Math.sinh = function(x) {
+    return 0.5 * (Math.exp(x) - Math.exp(-x))
+  }
+  /**
+    Returns the inverse hyperbolic sine of x.
+
+    @param x The value for x
+    @return The inverse hyperbolic sine of x
+    @addon
+    */
+  Math.asinh = function(x) {
+    return Math.log(x + Math.sqrt(x*x + 1))
+  }
+}
+if (!Math.cosh) {
+  /**
+    Returns the hyperbolic cosine of x.
+
+    @param x The value for x
+    @return The hyperbolic cosine of x
+    @addon
+    */
+  Math.cosh = function(x) {
+    return 0.5 * (Math.exp(x) + Math.exp(-x))
+  }
+  /**
+    Returns the inverse hyperbolic cosine of x.
+
+    @param x The value for x
+    @return The inverse hyperbolic cosine of x
+    @addon
+    */
+  Math.acosh = function(x) {
+    return Math.log(x + Math.sqrt(x*x - 1))
+  }
+}
+
+/**
+  Creates and configures a DOM element.
+
+  The tag of the element is given by name.
+
+  If params is a string, it is used as the innerHTML of the created element.
+  If params is a DOM element, it is appended to the created element.
+  If params is an object, it is treated as a config object and merged
+  with the created element.
+
+  If params is a string or DOM element, the third argument is treated
+  as the config object.
+
+  Special attributes of the config object:
+    * content
+      - if content is a string, it is used as the innerHTML of the
+        created element
+      - if content is an element, it is appended to the created element
+    * style
+      - the style object is merged with the created element's style
+
+  @param {String} name The tag for the created element
+  @param params The content or config for the created element
+  @param config The config for the created element if params is content
+  @return The created DOM element
+  */
+E = function(name, params, config) {
+  var el = document.createElement(name)
+  if (params) {
+    if (typeof(params) == 'string') {
+      el.innerHTML = params
+      params = config
+    } else if (params.DOCUMENT_NODE) {
+      el.appendChild(params)
+      params = config
+    }
+    if (params) {
+      if (params.style) {
+        var style = params.style
+        params = Object.clone(params)
+        delete params.style
+        Object.forceExtend(el.style, style)
+      }
+      if (params.content) {
+        if (typeof(params.content) == 'string') {
+          el.appendChild(T(params.content))
+        } else {
+          el.appendChild(params.content)
+        }
+        params = Object.clone(params)
+        delete params.content
+      }
+      Object.forceExtend(el, params)
+    }
+  }
+  return el
+}
+E.append = function(node) {
+  for(var i=1; i<arguments.length; i++) {
+    if (typeof(arguments[i]) == 'string') {
+      node.appendChild(T(arguments[i]))
+    } else {
+      node.appendChild(arguments[i])
+    }
+  }
+}
+// Safari requires each canvas to have a unique id.
+E.lastCanvasId = 0
+/**
+  Creates and returns a canvas element with width w and height h.
+
+  @param {int} w The width for the canvas
+  @param {int} h The height for the canvas
+  @param config Optional config object to pass to E()
+  @return The created canvas element
+  */
+E.canvas = function(w,h,config) {
+  var id = 'canvas-uuid-' + E.lastCanvasId
+  E.lastCanvasId++
+  if (!config) config = {}
+  return E('canvas', Object.extend(config, {id: id, width: w, height: h}))
+}
+
+/**
+  Shortcut for document.createTextNode.
+
+  @param {String} text The text for the text node
+  @return The created text node
+  */
+T = function(text) {
+  return document.createTextNode(text)
+}
+
+/**
+  Merges the src object's attributes with the dst object, ignoring errors.
+
+  @param dst The destination object
+  @param src The source object
+  @return The dst object
+  @addon
+  */
+Object.forceExtend = function(dst, src) {
+  for (var i in src) {
+    try{ dst[i] = src[i] } catch(e) {}
+  }
+  return dst
+}
+// In case Object.extend isn't defined already, set it to Object.forceExtend.
+if (!Object.extend)
+  Object.extend = Object.forceExtend
+
+/**
+  Merges the src object's attributes with the dst object, preserving all dst
+  object's current attributes.
+
+  @param dst The destination object
+  @param src The source object
+  @return The dst object
+  @addon
+  */
+Object.conditionalExtend = function(dst, src) {
+  for (var i in src) {
+    if (dst[i] == null)
+      dst[i] = src[i]
+  }
+  return dst
+}
+
+/**
+  Creates and returns a shallow copy of the src object.
+
+  @param src The source object
+  @return A clone of the src object
+  @addon
+  */
+Object.clone = function(src) {
+  if (!src || src == true)
+    return src
+  switch (typeof(src)) {
+    case 'string':
+      return Object.extend(src+'', src)
+      break
+    case 'number':
+      return src
+      break
+    case 'function':
+      obj = eval(src.toSource())
+      return Object.extend(obj, src)
+      break
+    case 'object':
+      if (src instanceof Array) {
+        return Object.extend([], src)
+      } else {
+        return Object.extend({}, src)
+      }
+      break
+  }
+}
+
+/**
+  Creates and returns an Image object, with source URL set to src and
+  onload handler set to onload.
+
+  @param {String} src The source URL for the image
+  @param {Function} onload The onload handler for the image
+  @return The created Image object
+  @type {Image}
+  */
+Object.loadImage = function(src, onload) {
+  var img = new Image()
+  if (onload)
+    img.onload = onload
+  img.src = src
+  return img
+}
+
+/**
+  Returns true if image is fully loaded and ready for use.
+
+  @param image The image to check
+  @return Whether the image is loaded or not
+  @type {boolean}
+  @addon
+  */
+Object.isImageLoaded = function(image) {
+  if (image.tagName == 'CANVAS') return true
+  if (!image.complete) return false
+  if (image.naturalWidth == null) return true
+  return !!image.naturalWidth
+}
+
+/**
+  Sums two objects.
+  */
+Object.sum = function(a,b) {
+  if (a instanceof Array) {
+    if (b instanceof Array) {
+      var ab = []
+      for (var i=0; i<a.length; i++) {
+        ab[i] = a[i] + b[i]
+      }
+      return ab
+    } else {
+      return a.map(function(v){ return v + b })
+    }
+  } else if (b instanceof Array) {
+    return b.map(function(v){ return v + a })
+  } else {
+    return a + b
+  }
+}
+
+/**
+  Substracts b from a.
+  */
+Object.sub = function(a,b) {
+  if (a instanceof Array) {
+    if (b instanceof Array) {
+      var ab = []
+      for (var i=0; i<a.length; i++) {
+        ab[i] = a[i] - b[i]
+      }
+      return ab
+    } else {
+      return a.map(function(v){ return v - b })
+    }
+  } else if (b instanceof Array) {
+    return b.map(function(v){ return a - v })
+  } else {
+    return a - b
+  }
+}
+
+if (!window.Mouse) Mouse = {}
+/**
+  Returns the coordinates for a mouse event relative to element.
+  Element must be the target for the event.
+
+  @param element The element to compare against
+  @param event The mouse event
+  @return An object of form {x: relative_x, y: relative_y}
+  */
+Mouse.getRelativeCoords = function(element, event) {
+  var xy = {x:0, y:0}
+  var osl = 0
+  var ost = 0
+  var el = element
+  while (el) {
+    osl += el.offsetLeft
+    ost += el.offsetTop
+    el = el.offsetParent
+  }
+  xy.x = event.pageX - osl
+  xy.y = event.pageY - ost
+  return xy
+}
+
+Browser = (function(){
+  var ua = window.navigator.userAgent
+  var khtml = ua.match(/KHTML/)
+  var gecko = ua.match(/Gecko/)
+  var webkit = ua.match(/WebKit\/\d+/)
+  var ie = ua.match(/Explorer/)
+  if (khtml) return 'KHTML'
+  if (gecko) return 'Gecko'
+  if (webkit) return 'Webkit'
+  if (ie) return 'IE'
+  return 'UNKNOWN'
+})()
+
+
+Mouse.LEFT = 0
+Mouse.MIDDLE = 1
+Mouse.RIGHT = 2
+
+if (Browser == 'IE') {
+  Mouse.LEFT = 1
+  Mouse.MIDDLE = 4
+}
+
+
+/**
+  Klass is a function that returns a constructor function.
+
+  The constructor function calls #initialize with its arguments.
+
+  The parameters to Klass have their prototypes or themselves merged with the
+  constructor function's prototype.
+
+  Finally, the constructor function's prototype is merged with the constructor
+  function. So you can write Shape.getArea.call(this) instead of
+  Shape.prototype.getArea.call(this).
+
+  Shape = Klass({
+    getArea : function() {
+      raise('No area defined!')
+    }
+  })
+
+  Rectangle = Klass(Shape, {
+    initialize : function(x, y) {
+      this.x = x
+      this.y = y
+    },
+
+    getArea : function() {
+      return this.x * this.y
+    }
+  })
+
+  Square = Klass(Rectangle, {
+    initialize : function(s) {
+      Rectangle.initialize.call(this, s, s)
+    }
+  })
+
+  new Square(5).getArea()
+  //=> 25
+
+  @return Constructor object for the class
+  */
+Klass = function() {
+  var c = function() {
+    this.initialize.apply(this, arguments)
+  }
+  c.ancestors = $A(arguments)
+  c.prototype = {}
+  for(var i = 0; i<arguments.length; i++) {
+    var a = arguments[i]
+    if (a.prototype) {
+      Object.extend(c.prototype, a.prototype)
+    } else {
+      Object.extend(c.prototype, a)
+    }
+  }
+  Object.extend(c, c.prototype)
+  return c
+}
+
+
+
+Curves = {
+
+  angularDistance : function(a, b) {
+    var pi2 = Math.PI*2
+    var d = (b - a) % pi2
+    if (d > Math.PI) d -= pi2
+    if (d < -Math.PI) d += pi2
+    return d
+  },
+
+  linePoint : function(a, b, t) {
+    return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t]
+  },
+
+  quadraticPoint : function(a, b, c, t) {
+    // var d = this.linePoint(a,b,t)
+    // var e = this.linePoint(b,c,t)
+    // return this.linePoint(d,e,t)
+    var dx = a[0]+(b[0]-a[0])*t
+    var ex = b[0]+(c[0]-b[0])*t
+    var x = dx+(ex-dx)*t
+    var dy = a[1]+(b[1]-a[1])*t
+    var ey = b[1]+(c[1]-b[1])*t
+    var y = dy+(ey-dy)*t
+    return [x,y]
+  },
+
+  cubicPoint : function(a, b, c, d, t) {
+    var ax3 = a[0]*3
+    var bx3 = b[0]*3
+    var cx3 = c[0]*3
+    var ay3 = a[1]*3
+    var by3 = b[1]*3
+    var cy3 = c[1]*3
+    return [
+      a[0] + t*(bx3 - ax3 + t*(ax3-2*bx3+cx3 + t*(bx3-a[0]-cx3+d[0]))),
+      a[1] + t*(by3 - ay3 + t*(ay3-2*by3+cy3 + t*(by3-a[1]-cy3+d[1])))
+    ]
+  },
+
+  linearValue : function(a,b,t) {
+    return a + (b-a)*t
+  },
+
+  quadraticValue : function(a,b,c,t) {
+    var d = a + (b-a)*t
+    var e = b + (c-b)*t
+    return d + (e-d)*t
+  },
+
+  cubicValue : function(a,b,c,d,t) {
+    var a3 = a*3, b3 = b*3, c3 = c*3
+    return a + t*(b3 - a3 + t*(a3-2*b3+c3 + t*(b3-a-c3+d)))
+  },
+
+  catmullRomPoint : function (a,b,c,d, t) {
+    var af = ((-t+2)*t-1)*t*0.5
+    var bf = (((3*t-5)*t)*t+2)*0.5
+    var cf = ((-3*t+4)*t+1)*t*0.5
+    var df = ((t-1)*t*t)*0.5
+    return [
+      a[0]*af + b[0]*bf + c[0]*cf + d[0]*df,
+      a[1]*af + b[1]*bf + c[1]*cf + d[1]*df
+    ]
+  },
+
+  catmullRomAngle : function (a,b,c,d, t) {
+    var dx = 0.5 * (c[0] - a[0] + 2*t*(2*a[0] - 5*b[0] + 4*c[0] - d[0]) +
+             3*t*t*(3*b[0] + d[0] - a[0] - 3*c[0]))
+    var dy = 0.5 * (c[1] - a[1] + 2*t*(2*a[1] - 5*b[1] + 4*c[1] - d[1]) +
+             3*t*t*(3*b[1] + d[1] - a[1] - 3*c[1]))
+    return Math.atan2(dy, dx)
+  },
+
+  catmullRomPointAngle : function (a,b,c,d, t) {
+    var p = this.catmullRomPoint(a,b,c,d,t)
+    var a = this.catmullRomAngle(a,b,c,d,t)
+    return {point:p, angle:a}
+  },
+
+  lineAngle : function(a,b) {
+    return Math.atan2(b[1]-a[1], b[0]-a[0])
+  },
+
+  quadraticAngle : function(a,b,c,t) {
+    var d = this.linePoint(a,b,t)
+    var e = this.linePoint(b,c,t)
+    return this.lineAngle(d,e)
+  },
+
+  cubicAngle : function(a, b, c, d, t) {
+    var e = this.quadraticPoint(a,b,c,t)
+    var f = this.quadraticPoint(b,c,d,t)
+    return this.lineAngle(e,f)
+  },
+
+  lineLength : function(a,b) {
+    var x = (b[0]-a[0])
+    var y = (b[1]-a[1])
+    return Math.sqrt(x*x + y*y)
+  },
+
+  squareLineLength : function(a,b) {
+    var x = (b[0]-a[0])
+    var y = (b[1]-a[1])
+    return x*x + y*y
+  },
+
+  quadraticLength : function(a,b,c, error) {
+    var p1 = this.linePoint(a,b,2/3)
+    var p2 = this.linePoint(b,c,1/3)
+    return this.cubicLength(a,p1,p2,c, error)
+  },
+
+  cubicLength : (function() {
+    var bezsplit = function(v) {
+      var vtemp = [v.slice(0)]
+
+      for (var i=1; i < 4; i++) {
+        vtemp[i] = [[],[],[],[]]
+        for (var j=0; j < 4-i; j++) {
+          vtemp[i][j][0] = 0.5 * (vtemp[i-1][j][0] + vtemp[i-1][j+1][0])
+          vtemp[i][j][1] = 0.5 * (vtemp[i-1][j][1] + vtemp[i-1][j+1][1])
+        }
+      }
+      var left = []
+      var right = []
+      for (var j=0; j<4; j++) {
+        left[j] = vtemp[j][0]
+        right[j] = vtemp[3-j][j]
+      }
+      return [left, right]
+    }
+
+    var addifclose = function(v, error) {
+      var len = 0
+      for (var i=0; i < 3; i++) {
+        len += Curves.lineLength(v[i], v[i+1])
+      }
+      var chord = Curves.lineLength(v[0], v[3])
+      if ((len - chord) > error) {
+        var lr = bezsplit(v)
+        len = addifclose(lr[0], error) + addifclose(lr[1], error)
+      }
+      return len
+    }
+
+    return function(a,b,c,d, error) {
+      if (!error) error = 1
+      return addifclose([a,b,c,d], error)
+    }
+  })(),
+
+  quadraticLengthPointAngle : function(a,b,c,lt,error) {
+    var p1 = this.linePoint(a,b,2/3)
+    var p2 = this.linePoint(b,c,1/3)
+    return this.cubicLengthPointAngle(a,p1,p2,c, error)
+  },
+
+  cubicLengthPointAngle : function(a,b,c,d,lt,error) {
+    // this thing outright rapes the GC.
+    // how about not creating a billion arrays, hmm?
+    var len = this.cubicLength(a,b,c,d,error)
+    var point = a
+    var prevpoint = a
+    var lengths = []
+    var prevlensum = 0
+    var lensum = 0
+    var tl = lt*len
+    var segs = 20
+    var fac = 1/segs
+    for (var i=1; i<=segs; i++) { // FIXME get smarter
+      prevpoint = point
+      point = this.cubicPoint(a,b,c,d, fac*i)
+      prevlensum = lensum
+      lensum += this.lineLength(prevpoint, point)
+      if (lensum >= tl) {
+        if (lensum == prevlensum)
+          return {point: point, angle: this.lineAngle(a,b)}
+        var dl = lensum - tl
+        var dt = dl / (lensum-prevlensum)
+        return {point: this.linePoint(prevpoint, point, 1-dt),
+                angle: this.cubicAngle(a,b,c,d, fac*(i-dt)) }
+      }
+    }
+    return {point: d.slice(0), angle: this.lineAngle(c,d)}
+  }
+
+}
+
+
+
+/**
+  Color helper functions.
+  */
+Colors = {
+
+  /**
+    Converts an HSL color to its corresponding RGB color.
+
+    @param h Hue in degrees (0 .. 359)
+    @param s Saturation (0.0 .. 1.0)
+    @param l Lightness (0 .. 255)
+    @return The corresponding RGB color as [r,g,b]
+    @type Array
+    */
+  hsl2rgb : function(h,s,l) {
+    var r,g,b
+    if (s == 0) {
+      r=g=b=v
+    } else {
+      var q = (l < 0.5 ? l * (1+s) : l+s-(l*s))
+      var p = 2 * l - q
+      var hk = (h % 360) / 360
+      var tr = hk + 1/3
+      var tg = hk
+      var tb = hk - 1/3
+      if (tr < 0) tr++
+      if (tr > 1) tr--
+      if (tg < 0) tg++
+      if (tg > 1) tg--
+      if (tb < 0) tb++
+      if (tb > 1) tb--
+      if (tr < 1/6)
+        r = p + ((q-p)*6*tr)
+      else if (tr < 1/2)
+        r = q
+      else if (tr < 2/3)
+        r = p + ((q-p)*6*(2/3 - tr))
+      else
+        r = p
+
+      if (tg < 1/6)
+        g = p + ((q-p)*6*tg)
+      else if (tg < 1/2)
+        g = q
+      else if (tg < 2/3)
+        g = p + ((q-p)*6*(2/3 - tg))
+      else
+        g = p
+
+      if (tb < 1/6)
+        b = p + ((q-p)*6*tb)
+      else if (tb < 1/2)
+        b = q
+      else if (tb < 2/3)
+        b = p + ((q-p)*6*(2/3 - tb))
+      else
+        b = p
+    }
+
+    return [r,g,b]
+  },
+
+  /**
+    Converts an HSV color to its corresponding RGB color.
+
+    @param h Hue in degrees (0 .. 359)
+    @param s Saturation (0.0 .. 1.0)
+    @param v Value (0 .. 255)
+    @return The corresponding RGB color as [r,g,b]
+    @type Array
+    */
+  hsv2rgb : function(h,s,v) {
+    var r,g,b
+    if (s == 0) {
+      r=g=b=v
+    } else {
+      h = (h % 360)/60.0
+      var i = Math.floor(h)
+      var f = h-i
+      var p = v * (1-s)
+      var q = v * (1-s*f)
+      var t = v * (1-s*(1-f))
+      switch (i) {
+        case 0:
+          r = v
+          g = t
+          b = p
+          break
+        case 1:
+          r = q
+          g = v
+          b = p
+          break
+        case 2:
+          r = p
+          g = v
+          b = t
+          break
+        case 3:
+          r = p
+          g = q
+          b = v
+          break
+        case 4:
+          r = t
+          g = p
+          b = v
+          break
+        case 5:
+          r = v
+          g = p
+          b = q
+          break
+      }
+    }
+    return [r,g,b]
+  },
+
+  /**
+    Parses a color style object into one that can be used with the given
+    canvas context.
+
+    Accepted formats:
+      'white'
+      '#fff'
+      '#ffffff'
+      'rgba(255,255,255, 1.0)'
+      [255, 255, 255]
+      [255, 255, 255, 1.0]
+      new Gradient(...)
+      new Pattern(...)
+
+    @param style The color style to parse
+    @param ctx Canvas 2D context on which the style is to be used
+    @return A parsed style, ready to be used as ctx.fillStyle / strokeStyle
+    */
+  parseColorStyle : function(style, ctx) {
+    if (typeof style == 'string') {
+      return style
+    } else if (style.compiled) {
+      return style.compiled
+    } else if (style.isPattern) {
+      return style.compile(ctx)
+    } else if (style.length == 3) {
+      return 'rgba('+style.map(Math.round).join(",")+', 1)'
+    } else if (style.length == 4) {
+      return 'rgba('+
+              Math.round(style[0])+','+
+              Math.round(style[1])+','+
+              Math.round(style[2])+','+
+              style[3]+
+             ')'
+    } else { // wtf
+      throw( "Bad style: " + style )
+    }
+  }
+}
+
+
+
+/**
+  Navigating around differing implementations of canvas features.
+
+  Current issues:
+
+  isPointInPath(x,y):
+
+    Opera supports isPointInPath.
+
+    Safari doesn't have isPointInPath. So you need to keep track of the CTM and
+    do your own in-fill-checking. Which is done for circles and rectangles
+    in Circle#isPointInPath and Rectangle#isPointInPath.
+    Paths use an inaccurate bounding box test, implemented in
+    Path#isPointInPath.
+
+    Firefox 3 has isPointInPath. But it uses user-space coordinates.
+    Which can be easily navigated around because it has setTransform.
+
+    Firefox 2 has isPointInPath. But it uses user-space coordinates.
+    And there's no setTransform, so you need to keep track of the CTM and
+    multiply the mouse vector with the CTM's inverse.
+
+  Drawing text:
+
+    Rhino has ctx.drawString(x,y, text)
+
+    Firefox has ctx.mozDrawText(text)
+
+    The WhatWG spec, Safari and Opera have nothing.
+
+*/
+CanvasSupport = {
+  DEVICE_SPACE : 0, // Opera
+  USER_SPACE : 1,   // Fx2, Fx3
+  isPointInPathMode : null,
+  supportsIsPointInPath : null,
+  supportsCSSTransform : null,
+  supportsCanvas : null,
+
+  isCanvasSupported : function() {
+    if (this.supportsCanvas == null) {
+      var e = {};
+      try { e = E('canvas'); } catch(x) {}
+      this.supportsCanvas = (e.getContext != null);
+    }
+    return this.supportsCanvas;
+  },
+
+  isCSSTransformSupported : function() {
+    if (this.supportsCSSTransform == null) {
+      var e = E('div')
+      var dbs = e.style
+      var s = (dbs.webkitTransform != null || dbs.MozTransform != null)
+      this.supportsCSSTransform = (s != null)
+    }
+    return this.supportsCSSTransform
+  },
+
+  getTestContext : function() {
+    if (!this.testContext) {
+      var c = E.canvas(1,1)
+      this.testContext = c.getContext('2d')
+    }
+    return this.testContext
+  },
+
+  getSupportsAudioTag : function() {
+    var e = E('audio')
+    return !!e.play
+  },
+
+  getSupportsSoundManager : function() {
+    return (window.soundManager && soundManager.enabled)
+  },
+
+  soundId : 0,
+
+  getSoundObject : function() {
+    var e = null
+//     if (this.getSupportsAudioTag()) {
+//       e = this.getAudioTagSoundObject()
+//     } else
+    if (this.getSupportsSoundManager()) {
+      e = this.getSoundManagerSoundObject()
+    }
+    return e
+  },
+
+  getAudioTagSoundObject : function() {
+    var sid = 'sound-' + this.soundId++
+    var e = E('audio', {id: sid})
+    e.load = function(src) {
+      this.src = src
+    }
+    e.addEventListener('canplaythrough', function() {
+      if (this.onready) this.onready()
+    }, false)
+    e.setVolume = function(v){ this.volume = v }
+    e.setPan = function(v){ this.pan = v }
+    return e
+  },
+
+  getSoundManagerSoundObject : function() {
+    var sid = 'sound-' + this.soundId++
+    var e = {
+      volume: 100,
+      pan: 0,
+      sid : sid,
+      load : function(src) {
+        return soundManager.load(this.sid, {
+          url: src,
+          autoPlay: false,
+          volume: this.volume,
+          pan: this.pan
+        })
+      },
+      _onload : function() {
+        if (this.onload) this.onload()
+        if (this.onready) this.onready()
+      },
+      _onerror : function() {
+        if (this.onerror) this.onerror()
+      },
+      _onfinish : function() {
+        if (this.onfinish) this.onfinish()
+      },
+      play : function() {
+        return soundManager.play(this.sid)
+      },
+      stop : function() {
+        return soundManager.stop(this.sid)
+      },
+      pause : function() {
+        return soundManager.togglePause(this.sid)
+      },
+      setVolume : function(v) {
+        this.volume = v*100
+        return soundManager.setVolume(this.sid, v*100)
+      },
+      setPan : function(v) {
+        this.pan = v*100
+        return soundManager.setPan(this.sid, v*100)
+      }
+    }
+    soundManager.createSound(sid, 'null.mp3')
+    e.sound = soundManager.getSoundById(sid)
+    e.sound.options.onfinish = e._onfinish.bind(e)
+    e.sound.options.onload = e._onload.bind(e)
+    e.sound.options.onerror = e._onerror.bind(e)
+    return e
+  },
+
+  /**
+    Canvas context augment module that adds setters.
+    */
+  ContextSetterAugment : {
+    setFillStyle : function(fs) { this.fillStyle = fs },
+    setStrokeStyle : function(ss) { this.strokeStyle = ss },
+    setGlobalAlpha : function(ga) { this.globalAlpha = ga },
+    setLineWidth : function(lw) { this.lineWidth = lw },
+    setLineCap : function(lw) { this.lineCap = lw },
+    setLineJoin : function(lw) { this.lineJoin = lw },
+    setMiterLimit : function(lw) { this.miterLimit = lw },
+    setGlobalCompositeOperation : function(lw) {
+      this.globalCompositeOperation = lw
+    },
+    setShadowColor : function(x) { this.shadowColor = x },
+    setShadowBlur : function(x) { this.shadowBlur = x },
+    setShadowOffsetX : function(x) { this.shadowOffsetX = x },
+    setShadowOffsetY : function(x) { this.shadowOffsetY = x },
+    setMozTextStyle : function(x) { this.mozTextStyle = x },
+    setFont : function(x) { this.font = x },
+    setTextAlign : function(x) { this.textAlign = x },
+    setTextBaseline : function(x) { this.textBaseline = x }
+  },
+
+  ContextJSImplAugment : {
+    identity : function() {
+      CanvasSupport.setTransform(this, [1,0,0,1,0,0])
+    }
+  },
+
+  /**
+    Augments a canvas context with setters.
+    */
+  augment : function(ctx) {
+    Object.conditionalExtend(ctx, this.ContextSetterAugment)
+    Object.conditionalExtend(ctx, this.ContextJSImplAugment)
+    return ctx
+  },
+
+  /**
+    Gets the augmented context for canvas.
+    */
+  getContext : function(canvas, type) {
+    var ctx = canvas.getContext(type || '2d')
+    this.augment(ctx)
+    return ctx
+  },
+
+
+  /**
+    Multiplies two 3x2 affine 2D column-major transformation matrices with
+    each other and stores the result in the first matrix.
+
+    Returns the multiplied matrix m1.
+    */
+  tMatrixMultiply : function(m1, m2) {
+    var m11 = m1[0]*m2[0] + m1[2]*m2[1]
+    var m12 = m1[1]*m2[0] + m1[3]*m2[1]
+
+    var m21 = m1[0]*m2[2] + m1[2]*m2[3]
+    var m22 = m1[1]*m2[2] + m1[3]*m2[3]
+
+    var dx = m1[0]*m2[4] + m1[2]*m2[5] + m1[4]
+    var dy = m1[1]*m2[4] + m1[3]*m2[5] + m1[5]
+
+    m1[0] = m11
+    m1[1] = m12
+    m1[2] = m21
+    m1[3] = m22
+    m1[4] = dx
+    m1[5] = dy
+
+    return m1
+  },
+
+  /**
+    Multiplies the vector [x, y, 1] with the 3x2 transformation matrix m.
+    */
+  tMatrixMultiplyPoint : function(m, x, y) {
+    return [
+      x*m[0] + y*m[2] + m[4],
+      x*m[1] + y*m[3] + m[5]
+    ]
+  },
+
+  /**
+    Inverts a 3x2 affine 2D column-major transformation matrix.
+
+    Returns an inverted copy of the matrix.
+    */
+  tInvertMatrix : function(m) {
+    var d = 1 / (m[0]*m[3]-m[1]*m[2])
+    return [
+      m[3]*d, -m[1]*d,
+      -m[2]*d, m[0]*d,
+      d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5])
+    ]
+  },
+
+  /**
+    Applies a transformation matrix m on the canvas context ctx.
+    */
+  transform : function(ctx, m) {
+    if (ctx.transform)
+      return ctx.transform.apply(ctx, m)
+    ctx.translate(m[4], m[5])
+    // scale
+    if (Math.abs(m[1]) < 1e-6 && Math.abs(m[2]) < 1e-6) {
+      ctx.scale(m[0], m[3])
+      return
+    }
+    var res = this.svdTransform({xx:m[0], xy:m[2], yx:m[1], yy:m[3], dx:m[4], dy:m[5]})
+    ctx.rotate(res.angle2)
+    ctx.scale(res.sx, res.sy)
+    ctx.rotate(res.angle1)
+    return
+  },
+
+  // broken svd...
+  brokenSvd : function(m) {
+    var mt = [m[0], m[2], m[1], m[3], 0,0]
+    var mtm = [
+      mt[0]*m[0]+mt[2]*m[1],
+      mt[1]*m[0]+mt[3]*m[1],
+      mt[0]*m[2]+mt[2]*m[3],
+      mt[1]*m[2]+mt[3]*m[3],
+      0,0
+    ]
+    // (mtm[0]-x) * (mtm[3]-x) - (mtm[1]*mtm[2]) = 0
+    // x*x - (mtm[0]+mtm[3])*x - (mtm[1]*mtm[2])+(mtm[0]*mtm[3]) = 0
+    var a = 1
+    var b = -(mtm[0]+mtm[3])
+    var c = -(mtm[1]*mtm[2])+(mtm[0]*mtm[3])
+    var d = Math.sqrt(b*b - 4*a*c)
+    var c1 = (-b + d) / (2*a)
+    var c2 = (-b - d) / (2*a)
+    if (c1 < c2)
+      var tmp = c1, c1 = c2, c2 = tmp
+    var s1 = Math.sqrt(c1)
+    var s2 = Math.sqrt(c2)
+    var i_s = [1/s1, 0, 0, 1/s2, 0,0]
+    // (mtm[0]-c1)*x1 + mtm[2]*x2 = 0
+    // mtm[1]*x1 + (mtm[3]-c1)*x2 = 0
+    // x2 = -(mtm[0]-c1)*x1 / mtm[2]
+    var e = ((mtm[0]-c1)/mtm[2])
+    var l = Math.sqrt(1 + e*e)
+    var v00 = 1 / l
+    var v10 = e / l
+    var v11 = v00
+    var v01 = -v10
+    var v = [v00, v01, v10, v11, 0,0]
+    var u = m.slice(0)
+    this.tMatrixMultiply(u,v)
+    this.tMatrixMultiply(u,i_s)
+    return [u, [s1,0,0,s2,0,0], [v00, v10, v01, v11, 0, 0]]
+  },
+
+
+  svdTransform : (function(){
+    //   Copyright (c) 2004-2005, The Dojo Foundation
+    //   All Rights Reserved
+    var m = {}
+    m.Matrix2D = function(arg){
+      // summary: a 2D matrix object
+      // description: Normalizes a 2D matrix-like object. If arrays is passed,
+      //    all objects of the array are normalized and multiplied sequentially.
+      // arg: Object
+      //    a 2D matrix-like object, a number, or an array of such objects
+      if(arg){
+        if(typeof arg == "number"){
+          this.xx = this.yy = arg;
+        }else if(arg instanceof Array){
+          if(arg.length > 0){
+            var matrix = m.normalize(arg[0]);
+            // combine matrices
+            for(var i = 1; i < arg.length; ++i){
+              var l = matrix, r = m.normalize(arg[i]);
+              matrix = new m.Matrix2D();
+              matrix.xx = l.xx * r.xx + l.xy * r.yx;
+              matrix.xy = l.xx * r.xy + l.xy * r.yy;
+              matrix.yx = l.yx * r.xx + l.yy * r.yx;
+              matrix.yy = l.yx * r.xy + l.yy * r.yy;
+              matrix.dx = l.xx * r.dx + l.xy * r.dy + l.dx;
+              matrix.dy = l.yx * r.dx + l.yy * r.dy + l.dy;
+            }
+            Object.extend(this, matrix);
+          }
+        }else{
+          Object.extend(this, arg);
+        }
+      }
+    }
+    // ensure matrix 2D conformance
+    m.normalize = function(matrix){
+        // summary: converts an object to a matrix, if necessary
+        // description: Converts any 2D matrix-like object or an array of
+        //    such objects to a valid dojox.gfx.matrix.Matrix2D object.
+        // matrix: Object: an object, which is converted to a matrix, if necessary
+        return (matrix instanceof m.Matrix2D) ? matrix : new m.Matrix2D(matrix); // dojox.gfx.matrix.Matrix2D
+    }
+    m.multiply = function(matrix){
+      // summary: combines matrices by multiplying them sequentially in the given order
+      // matrix: dojox.gfx.matrix.Matrix2D...: a 2D matrix-like object,
+      //    all subsequent arguments are matrix-like objects too
+      var M = m.normalize(matrix);
+      // combine matrices
+      for(var i = 1; i < arguments.length; ++i){
+        var l = M, r = m.normalize(arguments[i]);
+        M = new m.Matrix2D();
+        M.xx = l.xx * r.xx + l.xy * r.yx;
+        M.xy = l.xx * r.xy + l.xy * r.yy;
+        M.yx = l.yx * r.xx + l.yy * r.yx;
+        M.yy = l.yx * r.xy + l.yy * r.yy;
+        M.dx = l.xx * r.dx + l.xy * r.dy + l.dx;
+        M.dy = l.yx * r.dx + l.yy * r.dy + l.dy;
+      }
+      return M; // dojox.gfx.matrix.Matrix2D
+    }
+    m.invert = function(matrix) {
+      var M = m.normalize(matrix),
+        D = M.xx * M.yy - M.xy * M.yx,
+        M = new m.Matrix2D({
+          xx: M.yy/D, xy: -M.xy/D,
+          yx: -M.yx/D, yy: M.xx/D,
+          dx: (M.xy * M.dy - M.yy * M.dx) / D,
+          dy: (M.yx * M.dx - M.xx * M.dy) / D
+        });
+      return M; // dojox.gfx.matrix.Matrix2D
+    }
+    // the default (identity) matrix, which is used to fill in missing values
+    Object.extend(m.Matrix2D, {xx: 1, xy: 0, yx: 0, yy: 1, dx: 0, dy: 0});
+
+    var eq = function(/* Number */ a, /* Number */ b){
+      // summary: compare two FP numbers for equality
+      return Math.abs(a - b) <= 1e-6 * (Math.abs(a) + Math.abs(b)); // Boolean
+    };
+
+    var calcFromValues = function(/* Number */ s1, /* Number */ s2){
+      // summary: uses two close FP values to approximate the result
+      if(!isFinite(s1)){
+        return s2;  // Number
+      }else if(!isFinite(s2)){
+        return s1;  // Number
+      }
+      return (s1 + s2) / 2; // Number
+    };
+
+    var transpose = function(/* dojox.gfx.matrix.Matrix2D */ matrix){
+      // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object
+      var M = new m.Matrix2D(matrix);
+      return Object.extend(M, {dx: 0, dy: 0, xy: M.yx, yx: M.xy}); // dojox.gfx.matrix.Matrix2D
+    };
+
+    var scaleSign = function(/* dojox.gfx.matrix.Matrix2D */ matrix){
+      return (matrix.xx * matrix.yy < 0 || matrix.xy * matrix.yx > 0) ? -1 : 1; // Number
+    };
+
+    var eigenvalueDecomposition = function(/* dojox.gfx.matrix.Matrix2D */ matrix){
+      // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object
+      var M = m.normalize(matrix),
+        b = -M.xx - M.yy,
+        c = M.xx * M.yy - M.xy * M.yx,
+        d = Math.sqrt(b * b - 4 * c),
+        l1 = -(b + (b < 0 ? -d : d)) / 2,
+        l2 = c / l1,
+        vx1 = M.xy / (l1 - M.xx), vy1 = 1,
+        vx2 = M.xy / (l2 - M.xx), vy2 = 1;
+      if(eq(l1, l2)){
+        vx1 = 1, vy1 = 0, vx2 = 0, vy2 = 1;
+      }
+      if(!isFinite(vx1)){
+        vx1 = 1, vy1 = (l1 - M.xx) / M.xy;
+        if(!isFinite(vy1)){
+          vx1 = (l1 - M.yy) / M.yx, vy1 = 1;
+          if(!isFinite(vx1)){
+            vx1 = 1, vy1 = M.yx / (l1 - M.yy);
+          }
+        }
+      }
+      if(!isFinite(vx2)){
+        vx2 = 1, vy2 = (l2 - M.xx) / M.xy;
+        if(!isFinite(vy2)){
+          vx2 = (l2 - M.yy) / M.yx, vy2 = 1;
+          if(!isFinite(vx2)){
+            vx2 = 1, vy2 = M.yx / (l2 - M.yy);
+          }
+        }
+      }
+      var d1 = Math.sqrt(vx1 * vx1 + vy1 * vy1),
+        d2 = Math.sqrt(vx2 * vx2 + vy2 * vy2);
+      if(isNaN(vx1 /= d1)){ vx1 = 0; }
+      if(isNaN(vy1 /= d1)){ vy1 = 0; }
+      if(isNaN(vx2 /= d2)){ vx2 = 0; }
+      if(isNaN(vy2 /= d2)){ vy2 = 0; }
+      return {  // Object
+        value1: l1,
+        value2: l2,
+        vector1: {x: vx1, y: vy1},
+        vector2: {x: vx2, y: vy2}
+      };
+    };
+
+    var decomposeSR = function(/* dojox.gfx.matrix.Matrix2D */ M, /* Object */ result){
+      // summary: decomposes a matrix into [scale, rotate]; no checks are done.
+      var sign = scaleSign(M),
+        a = result.angle1 = (Math.atan2(M.yx, M.yy) + Math.atan2(-sign * M.xy, sign * M.xx)) / 2,
+        cos = Math.cos(a), sin = Math.sin(a);
+      result.sx = calcFromValues(M.xx / cos, -M.xy / sin);
+      result.sy = calcFromValues(M.yy / cos, M.yx / sin);
+      return result;  // Object
+    };
+
+    var decomposeRS = function(/* dojox.gfx.matrix.Matrix2D */ M, /* Object */ result){
+      // summary: decomposes a matrix into [rotate, scale]; no checks are done
+      var sign = scaleSign(M),
+        a = result.angle2 = (Math.atan2(sign * M.yx, sign * M.xx) + Math.atan2(-M.xy, M.yy)) / 2,
+        cos = Math.cos(a), sin = Math.sin(a);
+      result.sx = calcFromValues(M.xx / cos, M.yx / sin);
+      result.sy = calcFromValues(M.yy / cos, -M.xy / sin);
+      return result;  // Object
+    };
+
+    return function(matrix){
+      // summary: decompose a 2D matrix into translation, scaling, and rotation components
+      // description: this function decompose a matrix into four logical components:
+      //  translation, rotation, scaling, and one more rotation using SVD.
+      //  The components should be applied in following order:
+      //  | [translate, rotate(angle2), scale, rotate(angle1)]
+      // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object
+      var M = m.normalize(matrix),
+        result = {dx: M.dx, dy: M.dy, sx: 1, sy: 1, angle1: 0, angle2: 0};
+      // detect case: [scale]
+      if(eq(M.xy, 0) && eq(M.yx, 0)){
+        return Object.extend(result, {sx: M.xx, sy: M.yy});  // Object
+      }
+      // detect case: [scale, rotate]
+      if(eq(M.xx * M.yx, -M.xy * M.yy)){
+        return decomposeSR(M, result);  // Object
+      }
+      // detect case: [rotate, scale]
+      if(eq(M.xx * M.xy, -M.yx * M.yy)){
+        return decomposeRS(M, result);  // Object
+      }
+      // do SVD
+      var MT = transpose(M),
+        u  = eigenvalueDecomposition([M, MT]),
+        v  = eigenvalueDecomposition([MT, M]),
+        U  = new m.Matrix2D({xx: u.vector1.x, xy: u.vector2.x, yx: u.vector1.y, yy: u.vector2.y}),
+        VT = new m.Matrix2D({xx: v.vector1.x, xy: v.vector1.y, yx: v.vector2.x, yy: v.vector2.y}),
+        S = new m.Matrix2D([m.invert(U), M, m.invert(VT)]);
+      decomposeSR(VT, result);
+      S.xx *= result.sx;
+      S.yy *= result.sy;
+      decomposeRS(U, result);
+      S.xx *= result.sx;
+      S.yy *= result.sy;
+      return Object.extend(result, {sx: S.xx, sy: S.yy});  // Object
+    };
+  })(),
+
+
+  /**
+    Sets the canvas context ctx's transformation matrix to m, with ctm being
+    the current transformation matrix.
+    */
+  setTransform : function(ctx, m, ctm) {
+    if (ctx.setTransform)
+      return ctx.setTransform.apply(ctx, m)
+    this.transform(ctx, this.tInvertMatrix(ctm))
+    this.transform(ctx, m)
+  },
+
+  /**
+    Skews the canvas context by angle on the x-axis.
+    */
+  skewX : function(ctx, angle) {
+    return this.transform(ctx, this.tSkewXMatrix(angle))
+  },
+
+  /**
+    Skews the canvas context by angle on the y-axis.
+    */
+  skewY : function(ctx, angle) {
+    return this.transform(ctx, this.tSkewYMatrix(angle))
+  },
+
+  /**
+    Rotates a transformation matrix by angle.
+    */
+  tRotate : function(m1, angle) {
+    // return this.tMatrixMultiply(matrix, this.tRotationMatrix(angle))
+    var c = Math.cos(angle)
+    var s = Math.sin(angle)
+    var m11 = m1[0]*c + m1[2]*s
+    var m12 = m1[1]*c + m1[3]*s
+    var m21 = m1[0]*-s + m1[2]*c
+    var m22 = m1[1]*-s + m1[3]*c
+    m1[0] = m11
+    m1[1] = m12
+    m1[2] = m21
+    m1[3] = m22
+    return m1
+  },
+
+  /**
+    Translates a transformation matrix by x and y.
+    */
+  tTranslate : function(m1, x, y) {
+    // return this.tMatrixMultiply(matrix, this.tTranslationMatrix(x,y))
+    m1[4] += m1[0]*x + m1[2]*y
+    m1[5] += m1[1]*x + m1[3]*y
+    return m1
+  },
+
+  /**
+    Scales a transformation matrix by sx and sy.
+    */
+  tScale : function(m1, sx, sy) {
+    // return this.tMatrixMultiply(matrix, this.tScalingMatrix(sx,sy))
+    m1[0] *= sx
+    m1[1] *= sx
+    m1[2] *= sy
+    m1[3] *= sy
+    return m1
+  },
+
+  /**
+    Skews a transformation matrix by angle on the x-axis.
+    */
+  tSkewX : function(m1, angle) {
+    return this.tMatrixMultiply(m1, this.tSkewXMatrix(angle))
+  },
+
+  /**
+    Skews a transformation matrix by angle on the y-axis.
+    */
+  tSkewY : function(m1, angle) {
+    return this.tMatrixMultiply(m1, this.tSkewYMatrix(angle))
+  },
+
+  /**
+    Returns a 3x2 2D column-major y-skew matrix for the angle.
+    */
+  tSkewXMatrix : function(angle) {
+    return [ 1, 0, Math.tan(angle), 1, 0, 0 ]
+  },
+
+  /**
+    Returns a 3x2 2D column-major y-skew matrix for the angle.
+    */
+  tSkewYMatrix : function(angle) {
+    return [ 1, Math.tan(angle), 0, 1, 0, 0 ]
+  },
+
+  /**
+    Returns a 3x2 2D column-major rotation matrix for the angle.
+    */
+  tRotationMatrix : function(angle) {
+    var c = Math.cos(angle)
+    var s = Math.sin(angle)
+    return [ c, s, -s, c, 0, 0 ]
+  },
+
+  /**
+    Returns a 3x2 2D column-major translation matrix for x and y.
+    */
+  tTranslationMatrix : function(x, y) {
+    return [ 1, 0, 0, 1, x, y ]
+  },
+
+  /**
+    Returns a 3x2 2D column-major scaling matrix for sx and sy.
+    */
+  tScalingMatrix : function(sx, sy) {
+    return [ sx, 0, 0, sy, 0, 0 ]
+  },
+
+  /**
+    Returns the name of the text backend to use.
+
+    Possible values are:
+      * 'MozText' for Firefox
+      * 'DrawString' for Rhino
+      * 'NONE' no text drawing
+
+    @return The text backend name
+    @type String
+    */
+  getTextBackend : function() {
+    if (this.textBackend == null)
+      this.textBackend = this.detectTextBackend()
+    return this.textBackend
+  },
+
+  /**
+    Detects the name of the text backend to use.
+
+    Possible values are:
+      * 'MozText' for Firefox
+      * 'DrawString' for Rhino
+      * 'NONE' no text drawing
+
+    @return The text backend name
+    @type String
+    */
+  detectTextBackend : function() {
+    var ctx = this.getTestContext()
+    if (ctx.fillText) {
+      return 'HTML5'
+    } else if (ctx.mozDrawText) {
+      return 'MozText'
+    } else if (ctx.drawString) {
+      return 'DrawString'
+    }
+    return 'NONE'
+  },
+
+  getSupportsPutImageData : function() {
+    if (this.supportsPutImageData == null) {
+      var ctx = this.getTestContext()
+      var support = ctx.putImageData
+      if (support) {
+        try {
+          var idata = ctx.getImageData(0,0,1,1)
+          idata[0] = 255
+          idata[1] = 0
+          idata[2] = 255
+          idata[3] = 255
+          ctx.putImageData({width: 1, height: 1, data: idata}, 0, 0)
+          var idata = ctx.getImageData(0,0,1,1)
+          support = [255, 0, 255, 255].equals(idata.data)
+        } catch(e) {
+          support = false
+        }
+      }
+      this.supportsPutImageData = support
+    }
+    return support
+  },
+
+  /**
+    Returns true if the browser can be coaxed to work with
+    {@link CanvasSupport.isPointInPath}.
+
+    @return Whether the browser supports isPointInPath or not
+    @type boolean
+    */
+  getSupportsIsPointInPath : function() {
+    if (this.supportsIsPointInPath == null)
+      this.supportsIsPointInPath = !!this.getTestContext().isPointInPath
+    return this.supportsIsPointInPath
+  },
+
+  /**
+    Returns the coordinate system in which the isPointInPath of the
+    browser operates. Possible coordinate systems are
+    CanvasSupport.DEVICE_SPACE and CanvasSupport.USER_SPACE.
+
+    @return The coordinate system for the browser's isPointInPath
+    */
+  getIsPointInPathMode : function() {
+    if (this.isPointInPathMode == null)
+      this.isPointInPathMode = this.detectIsPointInPathMode()
+    return this.isPointInPathMode
+  },
+
+  /**
+    Detects the coordinate system in which the isPointInPath of the
+    browser operates. Possible coordinate systems are
+    CanvasSupport.DEVICE_SPACE and CanvasSupport.USER_SPACE.
+
+    @return The coordinate system for the browser's isPointInPath
+    @private
+    */
+  detectIsPointInPathMode : function() {
+    var ctx = this.getTestContext()
+    var rv
+    if (!ctx.isPointInPath)
+      return this.USER_SPACE
+    ctx.save()
+    ctx.translate(1,0)
+    ctx.beginPath()
+    ctx.rect(0,0,1,1)
+    if (ctx.isPointInPath(0.3,0.3)) {
+      rv = this.USER_SPACE
+    } else {
+      rv = this.DEVICE_SPACE
+    }
+    ctx.restore()
+    return rv
+  },
+
+  /**
+    Returns true if the device-space point (x,y) is inside the fill of
+    ctx's current path.
+
+    @param ctx Canvas 2D context to query
+    @param x The distance in pixels from the left side of the canvas element
+    @param y The distance in pixels from the top side of the canvas element
+    @param matrix The current transformation matrix. Needed if the browser has
+                  no isPointInPath or the browser's isPointInPath works in
+                  user-space coordinates and the browser doesn't support
+                  setTransform.
+    @param callbackObj If the browser doesn't support isPointInPath,
+                       callbackObj.isPointInPath will be called with the
+                       x,y-coordinates transformed to user-space.
+    @param
+    @return Whether (x,y) is inside ctx's current path or not
+    @type boolean
+    */
+  isPointInPath : function(ctx, x, y, matrix, callbackObj) {
+    var rv
+    if (!ctx.isPointInPath) {
+      if (callbackObj && callbackObj.isPointInPath) {
+        var xy = this.tMatrixMultiplyPoint(this.tInvertMatrix(matrix), x, y)
+        return callbackObj.isPointInPath(xy[0], xy[1])
+      } else {
+        return false
+      }
+    } else {
+      if (this.getIsPointInPathMode() == this.USER_SPACE) {
+        if (!ctx.setTransform) {
+          var xy = this.tMatrixMultiplyPoint(this.tInvertMatrix(matrix), x, y)
+          rv = ctx.isPointInPath(xy[0], xy[1])
+        } else {
+          ctx.save()
+          ctx.setTransform(1,0,0,1,0,0)
+          rv = ctx.isPointInPath(x,y)
+          ctx.restore()
+        }
+      } else {
+        rv = ctx.isPointInPath(x,y)
+      }
+      return rv
+    }
+  }
+}
+
+
+RecordingContext = Klass({
+  objectId : 0,
+  commands : [],
+  isMockObject : true,
+
+  initialize : function(commands) {
+    this.commands = commands || []
+    Object.conditionalExtend(this, this.getMockContext())
+  },
+
+  getMockContext : function() {
+    if (!RecordingContext.MockContext) {
+      var c = E.canvas(1,1)
+      var ctx = CanvasSupport.getContext(c, '2d')
+      var obj = {}
+      for (var i in ctx) {
+        if (typeof(ctx[i]) == 'function')
+          obj[i] = this.createRecordingFunction(i)
+        else
+          obj[i] = ctx[i]
+      }
+      obj.isPointInPath = null
+      obj.transform = null
+      obj.setTransform = null
+      RecordingContext.MockContext = obj
+    }
+    return RecordingContext.MockContext
+  },
+
+  createRecordingFunction : function(name){
+    if (name.search(/^set[A-Z]/) != -1 && name != 'setTransform') {
+      var varName = name.charAt(3).toLowerCase() + name.slice(4)
+      return function(){
+        this[varName] = arguments[0]
+        this.commands.push([name, $A(arguments)])
+      }
+    } else {
+      return function(){
+        this.commands.push([name, $A(arguments)])
+      }
+    }
+  },
+
+  clear : function(){
+    this.commands = []
+  },
+
+  getRecording : function() {
+    return this.commands
+  },
+
+  serialize : function(width, height) {
+    return '(' + {
+      width: width, height: height,
+      commands: this.getRecording()
+    }.toSource() + ')'
+  },
+
+  play : function(ctx) {
+    RecordingContext.play(ctx, this.getRecording())
+  },
+
+  createLinearGradient : function() {
+    var id = this.objectId++
+    this.commands.push([id, '=', 'createLinearGradient', $A(arguments)])
+    return new MockGradient(this, id)
+  },
+
+  createRadialGradient : function() {
+    var id = this.objectId++
+    this.commands.push([id, '=', 'createRadialGradient', $A(arguments)])
+    return new this.MockGradient(this, id)
+  },
+
+  createPattern : function() {
+    var id = this.objectId++
+    this.commands.push([id, '=', 'createPattern', $A(arguments)])
+    return new this.MockGradient(this, id)
+  },
+
+  MockGradient : Klass({
+    isMockObject : true,
+
+    initialize : function(recorder, id) {
+      this.recorder = recorder
+      this.id = id
+    },
+
+    addColorStop : function() {
+      this.recorder.commands.push([this.id, 'addColorStop', $A(arguments)])
+    },
+
+    toSource : function() {
+      return {id : this.id, isMockObject : true}.toSource()
+    }
+  })
+})
+RecordingContext.play = function(ctx, commands) {
+  var dictionary = []
+  for (var i=0; i<commands.length; i++) {
+    var cmd = commands[i]
+    if (cmd.length == 2) {
+      var args = cmd[1]
+      if (args[0] && args[0].isMockObject) {
+        ctx[cmd[0]](dictionary[args[0].id])
+      } else {
+        ctx[cmd[0]].apply(ctx, cmd[1])
+      }
+    } else if (cmd.length == 3) {
+      var obj = dictionary[cmd[0]]
+      obj[cmd[1]].apply(obj, cmd[2])
+    } else if (cmd.length == 4) {
+      dictionary[cmd[0]] = ctx[cmd[2]].apply(ctx, cmd[3])
+    } else {
+      throw "Malformed command: "+cmd.toString()
+    }
+  }
+}
+
+
+
+
+Transformable = Klass({
+  needMatrixUpdate : true,
+
+  /**
+    Transforms the context state according to this node's attributes.
+
+    @param ctx Canvas 2D context
+    */
+  transform : function(ctx) {
+    var atm = this.absoluteMatrix
+    var xy = this.x || this.y
+    var rot = this.rotation
+    var sca = this.scale != null
+    var skX = this.skewX
+    var skY = this.skewY
+    var tm = this.matrix
+    var tl = this.transformList
+
+    // update the node's transformation matrix
+    if (this.needMatrixUpdate || !this.currentMatrix) {
+      if (!this.currentMatrix) this.currentMatrix = [1,0,0,1,0,0]
+      if (this.parent)
+        this.__copyMatrix(this.parent.currentMatrix)
+      else
+        this.__identityMatrix()
+      if (atm) this.__setMatrixMatrix(this.absoluteMatrix)
+      if (xy) this.__translateMatrix(this.x, this.y)
+      if (rot) this.__rotateMatrix(this.rotation)
+      if (skX) this.__skewXMatrix(this.skewX)
+      if (skY) this.__skewYMatrix(this.skewY)
+      if (sca) this.__scaleMatrix(this.scale)
+      if (tm) this.__matrixMatrix(this.matrix)
+      if (tl) {
+        for (var i=0; i<this.transformList.length; i++) {
+          var tl = this.transformList[i]
+          this['__'+tl[0]+'Matrix'](tl[1])
+        }
+      }
+      this.needMatrixUpdate = false
+    }
+
+    if (!ctx) return
+
+    // transform matrix modifiers
+    this.__setMatrix(ctx, this.currentMatrix)
+  },
+
+  distanceTo : function(node) {
+    return Curves.lineLength([this.x, this.y], [node.x, node.y])
+  },
+
+  angleTo : function(node) {
+    return Curves.lineAngle([this.x, this.y], [node.x, node.y])
+  },
+
+
+
+  __setMatrixMatrix : function(matrix) {
+    if (!this.previousMatrix) this.previousMatrix = []
+    var p = this.previousMatrix
+    var c = this.currentMatrix
+    p[0] = c[0]
+    p[1] = c[1]
+    p[2] = c[2]
+    p[3] = c[3]
+    p[4] = c[4]
+    p[5] = c[5]
+    p = this.currentMatrix
+    c = matrix
+    p[0] = c[0]
+    p[1] = c[1]
+    p[2] = c[2]
+    p[3] = c[3]
+    p[4] = c[4]
+    p[5] = c[5]
+  },
+
+  __copyMatrix : function(matrix) {
+    var p = this.currentMatrix
+    var c = matrix
+    p[0] = c[0]
+    p[1] = c[1]
+    p[2] = c[2]
+    p[3] = c[3]
+    p[4] = c[4]
+    p[5] = c[5]
+  },
+
+  __identityMatrix : function() {
+    var p = this.currentMatrix
+    p[0] = 1
+    p[1] = 0
+    p[2] = 0
+    p[3] = 1
+    p[4] = 0
+    p[5] = 0
+  },
+
+  __translateMatrix : function(x, y) {
+    if (x.length) {
+      CanvasSupport.tTranslate( this.currentMatrix, x[0], x[1] )
+    } else {
+      CanvasSupport.tTranslate( this.currentMatrix, x, y )
+    }
+  },
+
+  __rotateMatrix : function(rotation) {
+    if (rotation.length) {
+      if (rotation[0] % Math.PI*2 == 0) return
+      if (rotation[1] || rotation[2]) {
+        CanvasSupport.tTranslate( this.currentMatrix,
+                                  rotation[1], rotation[2] )
+        CanvasSupport.tRotate( this.currentMatrix, rotation[0] )
+        CanvasSupport.tTranslate( this.currentMatrix,
+                                  -rotation[1], -rotation[2] )
+      } else {
+        CanvasSupport.tRotate( this.currentMatrix, rotation[0] )
+      }
+    } else {
+      if (rotation % Math.PI*2 == 0) return
+      CanvasSupport.tRotate( this.currentMatrix, rotation )
+    }
+  },
+
+  __skewXMatrix : function(skewX) {
+    if (skewX.length && skewX[0])
+      CanvasSupport.tSkewX(this.currentMatrix, skewX[0])
+    else
+      CanvasSupport.tSkewX(this.currentMatrix, skewX)
+  },
+
+  __skewYMatrix : function(skewY) {
+    if (skewY.length && skewY[0])
+      CanvasSupport.tSkewY(this.currentMatrix, skewY[0])
+    else
+      CanvasSupport.tSkewY(this.currentMatrix, skewY)
+  },
+
+  __scaleMatrix : function(scale) {
+    if (scale.length == 2) {
+      if (scale[0] == 1 && scale[1] == 1) return
+      CanvasSupport.tScale(this.currentMatrix,
+                           scale[0], scale[1])
+    } else if (scale.length == 3) {
+      if (scale[0] == 1 || (scale[0].length && (scale[0][0] == 1 && scale[0][1] == 1)))
+        return
+      CanvasSupport.tTranslate(this.currentMatrix,
+                               scale[1], scale[2])
+      if (scale[0].length) {
+        CanvasSupport.tScale(this.currentMatrix,
+                              scale[0][0], scale[0][1])
+      } else {
+        CanvasSupport.tScale( this.currentMatrix, scale[0], scale[0] )
+      }
+      CanvasSupport.tTranslate(this.currentMatrix,
+                               -scale[1], -scale[2])
+    } else if (scale != 1) {
+      CanvasSupport.tScale( this.currentMatrix, scale, scale )
+    }
+  },
+
+  __matrixMatrix : function(matrix) {
+    CanvasSupport.tMatrixMultiply(this.currentMatrix, matrix)
+  },
+
+  __setMatrix : function(ctx, matrix) {
+    CanvasSupport.setTransform(ctx, matrix, this.previousMatrix)
+  },
+
+  __translate : function(ctx, x,y) {
+    if (x.length != null)
+      ctx.translate(x[0], x[1])
+    else
+      ctx.translate(x, y)
+  },
+
+  __rotate : function(ctx, rotation) {
+    if (rotation.length) {
+      if (rotation[1] || rotation[2]) {
+        if (rotation[0] % Math.PI*2 == 0) return
+        ctx.translate( rotation[1], rotation[2] )
+        ctx.rotate( rotation[0] )
+        ctx.translate( -rotation[1], -rotation[2] )
+      } else {
+        ctx.rotate( rotation[0] )
+      }
+    } else {
+      ctx.rotate( rotation )
+    }
+  },
+
+  __skewX : function(ctx, skewX) {
+    if (skewX.length && skewX[0])
+      CanvasSupport.skewX(ctx, skewX[0])
+    else
+      CanvasSupport.skewX(ctx, skewX)
+  },
+
+  __skewY : function(ctx, skewY) {
+    if (skewY.length && skewY[0])
+      CanvasSupport.skewY(ctx, skewY[0])
+    else
+      CanvasSupport.skewY(ctx, skewY)
+  },
+
+  __scale : function(ctx, scale) {
+    if (scale.length == 2) {
+      ctx.scale(scale[0], scale[1])
+    } else if (scale.length == 3) {
+      ctx.translate( scale[1], scale[2] )
+      if (scale[0].length) {
+        ctx.scale(scale[0][0], scale[0][1])
+      } else {
+        ctx.scale(scale[0], scale[0])
+      }
+      ctx.translate( -scale[1], -scale[2] )
+    } else {
+      ctx.scale(scale, scale)
+    }
+  },
+
+  __matrix : function(ctx, matrix) {
+    CanvasSupport.transform(ctx, matrix)
+  }
+
+})
+
+
+/**
+  Timeline is an animator that tweens between its frames.
+
+  When object.time = k.time:
+       object.state = k.state
+  When object.time > k[i-1].time and object.time < k[i].time:
+       object.state = k[i].tween(position, k[i-1].state, k[i].state)
+       where position = elapsed / duration,
+             elapsed = object.time - k[i-1].time,
+             duration = k[i].time - k[i-1].time
+  */
+Timeline = Klass({
+  startTime : null,
+  repeat : false,
+  lastAction : 0,
+
+  initialize : function(repeat, pingpong) {
+    this.repeat = repeat
+    this.keyframes = []
+  },
+
+  addKeyframe : function(time, target, tween) {
+    if (arguments.length == 1) this.keyframes.push(time)
+    else  this.keyframes.push({
+            time : time,
+            target : target,
+            tween : tween
+          })
+  },
+
+  appendKeyframe : function(timeDelta, target, tween) {
+    this.lastAction += timeDelta
+    return this.addKeyframe(this.lastAction, target, tween)
+  },
+
+  evaluate : function(object, ot, dt) {
+    if (this.startTime == null) this.startTime = ot
+    var t = ot - this.startTime
+    if (this.keyframes.length > 0) {
+      // find current keyframe
+      var currentIndex, previousFrame, currentFrame
+      for (var i=0; i<this.keyframes.length; i++) {
+        if (this.keyframes[i].time > t) {
+          currentIndex = i
+          break
+        }
+      }
+      if (currentIndex != null) {
+        previousFrame = this.keyframes[currentIndex-1]
+        currentFrame = this.keyframes[currentIndex]
+      }
+      if (!currentFrame) {
+        if (!this.keyframes.atEnd) {
+          this.keyframes.atEnd = true
+          previousFrame = this.keyframes[this.keyframes.length - 1]
+          Object.extend(object, Object.clone(previousFrame.target))
+          if (this.repeat) this.startTime = ot
+          object.changed = true
+        }
+      } else if (previousFrame) {
+        this.keyframes.atEnd = false
+        // animate towards current keyframe
+        var elapsed = t - previousFrame.time
+        var duration = currentFrame.time - previousFrame.time
+        var pos = elapsed / duration
+        for (var k in currentFrame.target) {
+          if (previousFrame.target[k] != null) {
+            object.tweenVariable(k,
+              previousFrame.target[k], currentFrame.target[k],
+              pos, currentFrame.tween)
+          }
+        }
+      }
+    }
+  }
+
+})
+
+
+Animatable = Klass({
+  tweenFunctions : {
+    linear : function(v) { return v },
+
+    set : function(v) { return Math.floor(v) },
+    discrete : function(v) { return Math.floor(v) },
+
+    sine : function(v) { return 0.5-0.5*Math.cos(v*Math.PI) },
+
+    sproing : function(v) {
+      return (0.5-0.5*Math.cos(v*3.59261946538606)) * 1.05263157894737
+      // pi + pi-acos(0.9)
+    },
+
+    square : function(v) {
+      return v*v
+    },
+
+    cube : function(v) {
+      return v*v*v
+    },
+
+    sqrt : function(v) {
+      return Math.sqrt(v)