/*
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)
    },

    curt : function(v) {
      return Math.pow(v, -0.333333333333)
    }
  },

  initialize : function() {
    this.lastAction = 0
    this.timeline = []
    this.keyframes = []
    this.pendingKeyframes = []
    this.pendingTimelineEvents = []
    this.timelines = []
    this.animators = []
    this.addFrameListener(this.updateTimelines)
    this.addFrameListener(this.updateKeyframes)
    this.addFrameListener(this.updateTimeline)
    this.addFrameListener(this.updateAnimators)
  },

  updateTimelines : function(t, dt) {
    for (var i=0; i<this.timelines.length; i++)
      this.timelines[i].evaluate(this, t, dt)
  },

  addTimeline : function(tl) {
    this.timelines.push(tl)
  },

  removeTimeline : function(tl) {
    this.timelines.deleteFirst(tl)
  },

  /**
    Tweens between keyframes (a keyframe is an object with the new values of
    the members of this, e.g. { time: 0, target: { x: 10, y: 20 }, tween: 'square'})

    Keyframes are very much like multi-variable animators, the main difference
    is that with keyframes the start value and the duration are implicit.

    While an animation from value A to B would take two keyframes instead of
    a single animator, chaining and reordering keyframes is very easy.
    */
  updateKeyframes : function(t,dt) {
    this.addPendingKeyframes(t)
    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(this, Object.clone(previousFrame.target))
          this.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) {
            this.tweenVariable(k,
              previousFrame.target[k], currentFrame.target[k],
              pos, currentFrame.tween)
          }
        }
      }
    }
  },

  addPendingKeyframes : function(t) {
    if (this.pendingKeyframes.length > 0) {
      while (this.pendingKeyframes.length > 0) {
        var kf = this.pendingKeyframes.shift()
        if (kf.time == null)
          kf.time = kf.relativeTime + t
        this.keyframes.push(kf)
      }
      this.keyframes.stableSort(function(a,b) { return a.time - b.time })
    }
  },

  /**
    Run and remove timelineEvents that have startTime <= t.
    TimelineEvents are run in the ascending order of their startTimes.
    */
  updateTimeline : function(t, dt) {
    this.addPendingTimelineEvents(t)
    while (this.timeline[0] && this.timeline[0].startTime <= t) {
      var keyframe = this.timeline.shift()
      var rv = true
      if (typeof(keyframe.action) == 'function')
        rv = keyframe.action.call(this, t, dt, keyframe)
      else
        this.animators.push(keyframe.action)
      if (keyframe.repeatEvery != null && rv != false) {
        if (keyframe.repeatTimes != null) {
          if (keyframe.repeatTimes <= 0) continue
          keyframe.repeatTimes--
        }
        keyframe.startTime += keyframe.repeatEvery
        this.addTimelineEvent(keyframe)
      }
      this.changed = true
    }
  },

  addPendingTimelineEvents : function(t) {
    if (this.pendingTimelineEvents.length > 0) {
      while (this.pendingTimelineEvents.length > 0) {
        var kf = this.pendingTimelineEvents.shift()
        if (!kf.startTime)
          kf.startTime = kf.relativeStartTime + t
        this.timeline.push(kf)
      }
      this.timeline.stableSort(function(a,b) { return a.startTime - b.startTime })
    }
  },

  addTimelineEvent : function(kf) {
    this.pendingTimelineEvents.push(kf)
  },

  /**
    Run each animator, delete ones that have their durations exceeded.
    */
  updateAnimators : function(t, dt) {
    for (var i=0; i<this.animators.length; i++) {
      var ani = this.animators[i]
      if (!ani.startTime) ani.startTime = t
      var elapsed = t - ani.startTime
      var pos = elapsed / ani.duration
      var shouldRemove = false
      if (pos >= 1) {
        if (!ani.repeat) {
          pos = 1
          shouldRemove = true
        } else {
          if (ani.repeat !== true) ani.repeat = Math.max(0, ani.repeat - 1)
          if (ani.accumulate) {
            ani.startValue = Object.clone(ani.endValue)
            ani.endValue = Object.sum(ani.difference, ani.endValue)
          }
          if (ani.repeat == 0) {
            shouldRemove = true
            pos = 1
          } else {
            ani.startTime = t
            pos = pos % 1
          }
        }
      } else if (ani.repeat && ani.repeat !== true && ani.repeat <= pos) {
        shouldRemove = true
        pos = ani.repeat
      }
      this.tweenVariable(ani.variable, ani.startValue, ani.endValue, pos, ani.tween)
      if (shouldRemove) {
        this.animators.splice(i, 1)
        i--
      }
    }
  },

  tweenVariable : function(variable, start, end, pos, tweenFunction) {
    if (typeof(tweenFunction) != 'function') {
      tweenFunction = this.tweenFunctions[tweenFunction] || this.tweenFunctions.linear
    }
    var tweened = tweenFunction(pos)
    if (typeof(variable) != 'function') {
      if (start instanceof Array) {
        for (var j=0; j<start.length; j++) {
          this[variable][j] = start[j] + tweened*(end[j]-start[j])
        }
      } else {
        this[variable] = start + tweened*(end-start)
      }
    } else {
      variable.call(this, tweened, start, end)
    }
    this.changed = true
  },

  animate : function(variable, start, end, duration, tween, config) {
    var start = Object.clone(start)
    var end = Object.clone(end)
    if (!config) config = {}
    if (config.additive) {
      var diff = Object.sub(end, start)
      start = Object.sum(start, this[variable])
      end = Object.sum(end, this[variable])
    }
    if (typeof(variable) != 'function')
      this[variable] = Object.clone(start)
    var ani = {
      id : Animatable.uid++,
      variable : variable,
      startValue : start,
      endValue : end,
      difference : diff,
      duration : duration,
      tween : tween,
      repeat : config.repeat,
      additive : config.additive,
      accumulate : config.accumulate,
      pingpong : config.pingpong
    }
    this.animators.push(ani)
    return ani
  },

  removeAnimator : function(animator) {
    this.animators.deleteFirst(animator)
  },

  animateTo : function(variableName, end, duration, tween, config) {
    return this.animate(variableName, this[variableName], end, duration, tween, config)
  },

  animateFrom : function(variableName, start, duration, tween, config) {
    return this.animate(variableName, start, this[variableName], duration, tween, config)
  },

  animateFactor : function(variableName, start, endFactor, duration, tween, config) {
    var end
    if (start instanceof Array) {
      end = []
      for (var i=0; i<start.length; i++) {
        end[i] = start[i] * endFactor
      }
    } else {
      end = start * endFactor
    }
    return this.animate(variableName, start, end, duration, tween, config)
  },

  animateToFactor : function(variableName, endFactor, duration, tween, config) {
    var start = this[variableName]
    return this.animateFactor(variableName, start, endFactor, duration, tween, config)
  },

  addKeyframe : function(time, target, tween) {
    var kf = {
      relativeTime: time,
      target: target,
      tween: tween
    }
    this.pendingKeyframes.push(kf)
  },

  addKeyframeAt : function(time, target, tween) {
    var kf = {
      time: time,
      target: target,
      tween: tween
    }
    this.pendingKeyframes.push(kf)
  },

  appendKeyframe : function(timeDelta, target, tween) {
    this.lastAction += timeDelta
    return this.addKeyframe(this.lastAction, target, tween)
  },

  every : function(duration, action, noFirst) {
    var kf = {
      action : action,
      relativeStartTime : noFirst ? duration : 0,
      repeatEvery : duration
    }
    this.addTimelineEvent(kf)
    return kf
  },

  at : function(time, action) {
    var kf = {
      action : action,
      startTime : time
    }
    this.addTimelineEvent(kf)
    return kf
  },

  after : function(duration, action) {
    var kf = {
      action : action,
      relativeStartTime : duration
    }
    this.addTimelineEvent(kf)
    return kf
  },

  afterFrame : function(duration, callback) {
    var elapsed = 0
    var animator
    animator = function(t, dt){
      if (elapsed >= duration) {
        callback.call(this)
        this.removeFrameListener(animator)
      }
      elapsed++
    }
    this.addFrameListener(animator)
    return animator
  },

  everyFrame : function(duration, callback, noFirst) {
    var elapsed = noFirst ? 0 : duration
    var animator
    animator = function(t, dt){
      if (elapsed >= duration) {
        if (callback.call(this) == false)
          this.removeFrameListener(animator)
        elapsed = 0
      }
      elapsed++
    }
    this.addFrameListener(animator)
    return animator
  }
})
Animatable.uid = 0



/**
  CanvasNode is the base CAKE scenegraph node. All the other scenegraph nodes
  derive from it. A plain CanvasNode does no drawing, but it can be used for
  grouping other nodes and setting up the group's drawing state.

  var scene = new CanvasNode({x: 10, y: 10})

  The usual way to use CanvasNodes is to append them to a Canvas object:

    var scene = new CanvasNode()
    scene.append(new Rectangle(40, 40, {fill: true}))
    var elem = E.canvas(400, 400)
    var canvas = new Canvas(elem)
    canvas.append(scene)

  You can also use CanvasNodes to draw directly to a canvas element:

    var scene = new CanvasNode()
    scene.append(new Circle(40, {x:200, y:200, stroke: true}))
    var elem = E.canvas(400, 400)
    scene.handleDraw(elem.getContext('2d'))

  */
CanvasNode = Klass(Animatable, Transformable, {
  OBJECTBOUNDINGBOX : 'objectBoundingBox',

  // whether to draw the node and its childNodes or not
  visible : true,

  // whether to draw the node (doesn't affect subtree)
  drawable : true,

  // the CSS display property can be used to affect 'visible'
  // false     => visible = visible
  // 'none'    => visible = false
  // otherwise => visible = true
  display : null,

  // the CSS visibility property can be used to affect 'drawable'
  // false     => drawable = drawable
  // 'hidden'  => drawable = false
  // otherwise => drawable = true
  visibility : null,

  // whether this and the subtree from this register mouse hover
  catchMouse : true,

  // Whether this object registers mouse hover. Only set this to true when you
  // have a drawable object that can be picked. Otherwise the object requires
  // a matrix inversion on Firefox 2 and Safari, which is slow.
  pickable : false,

  // true if this node or one of its descendants is under the mouse
  // cursor and catchMouse is true
  underCursor : false,

  // zIndex in relation to sibling nodes (note: not global)
  zIndex : 0,

  // x translation of the node
  x : 0,

  // y translation of the node
  y : 0,

  // scale factor: number for uniform scaling, [x,y] for dimension-wise
  scale : 1,

  // Rotation of the node, in radians.
  //
  // The rotation can also be the array [angle, cx, cy],
  // where cx and cy define the rotation center.
  //
  // The array form is equivalent to
  // translate(cx, cy); rotate(angle); translate(-cx, -cy);
  rotation : 0,

  // Transform matrix with which to multiply the current transform matrix.
  // Applied after all other transformations.
  matrix : null,

  // Transform matrix with which to replace the current transform matrix.
  // Applied before any other transformation.
  absoluteMatrix : null,

  // SVG-like list of transformations to apply.
  // The different transformations are:
  // ['translate', [x,y]]
  // ['rotate', [angle, cx, cy]] - (optional) cx and cy are the rotation center
  // ['scale', [x,y]]
  // ['matrix', [m11, m12, m21, m22, dx, dy]]
  transformList : null,

  // fillStyle for the node and its descendants
  // Possibilities:
  //   null // use the previous
  //   true      // use the previous but do fill
  //   false     // use the previous but don't do fill
  //   'none'    // use the previous but don't do fill
  //
  //   'white'
  //   '#fff'
  //   '#ffffff'
  //   'rgba(255,255,255, 1.0)'
  //   [255, 255, 255, 1.0]
  //   new Gradient(...)
  //   new Pattern(myImage, 'no-repeat')
  fill : null,

  // strokeStyle for the node and its descendants
  // Possibilities:
  //   null // use the previous
  //   true      // use the previous but do stroke
  //   false     // use the previous but don't do stroke
  //   'none'    // use the previous but don't do stroke
  //
  //   'white'
  //   '#fff'
  //   '#ffffff'
  //   'rgba(255,255,255, 1.0)'
  //   [255, 255, 255, 1.0]
  //   new Gradient(...)
  //   new Pattern(myImage, 'no-repeat')
  stroke : null,

  // stroke line width
  strokeWidth : null,

  // stroke line cap style ('butt' | 'round' | 'square')
  lineCap : null,

  // stroke line join style ('bevel' | 'round' | 'miter')
  lineJoin : null,

  // stroke line miter limit
  miterLimit : null,

  // set globalAlpha to this value
  absoluteOpacity : null,

  // multiply globalAlpha by this value
  opacity : null,

  // fill opacity
  fillOpacity : null,

  // stroke opacity
  strokeOpacity : null,

  // set globalCompositeOperation to this value
  // Possibilities:
  // ( 'source-over' |
  //   'copy' |
  //   'lighter' |
  //   'darker' |
  //   'xor' |
  //   'source-in' |
  //   'source-out' |
  //   'destination-over' |
  //   'destination-atop' |
  //   'destination-in' |
  //   'destination-out' )
  compositeOperation : null,

  // Color for the drop shadow
  shadowColor : null,

  // Drop shadow blur radius
  shadowBlur : null,

  // Drop shadow's x-offset
  shadowOffsetX : null,

  // Drop shadow's y-offset
  shadowOffsetY : null,

  // HTML5 text API
  font : null,
  // horizontal position of the text origin
  // 'left' | 'center' | 'right' | 'start' | 'end'
  textAlign : null,
  // vertical position of the text origin
  // 'top' | 'hanging' | 'middle' | 'alphabetic' | 'ideographic' | 'bottom'
  textBaseline : null,

  cursor : null,

  changed : true,

  tagName : 'g',

  getNextSibling : function(){
    if (this.parentNode)
      return this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this)+1]
    return null
  },

  getPreviousSibling : function(){
    if (this.parentNode)
      return this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this)-1]
    return null
  },

  /**
    Initialize the CanvasNode and merge an optional config hash.
    */
  initialize : function(config) {
    this.root = this
    this.currentMatrix = [1,0,0,1,0,0]
    this.previousMatrix = [1,0,0,1,0,0]
    this.needMatrixUpdate = true
    this.childNodes = []
    this.frameListeners = []
    this.eventListeners = {}
    Animatable.initialize.call(this)
    if (config)
      Object.extend(this, config)
  },

  /**
    Create a clone of the node and its subtree.
    */
  clone : function() {
    var c = Object.clone(this)
    c.parent = c.root = null
    for (var i in this) {
      if (typeof(this[i]) == 'object')
        c[i] = Object.clone(this[i])
    }
    c.parent = c.root = null
    c.childNodes = []
    c.setRoot(null)
    for (var i=0; i<this.childNodes.length; i++) {
      var ch = this.childNodes[i].clone()
      c.append(ch)
    }
    return c
  },

  cloneNode : function(){ return this.clone() },

  /**
    Gets node by id.
    */
  getElementById : function(id) {
    if (this.id == id)
      return this
    for (var i=0; i<this.childNodes.length; i++) {
      var n = this.childNodes[i].getElementById(id)
      if (n) return n
    }
    return null
  },

  $ : function(id) {
    return this.getElementById(id)
  },

  /**
    Alias for append().

    @param Node[s] to append
    */
  appendChild : function() {
    return this.append.apply(this, arguments)
  },

  /**
    Appends arguments as childNodes to the node.

    Adding a child sets child.parent to be the node and calls
    child.setRoot(node.root)

    @param Node[s] to append
    */
  append : function(obj) {
    var a = $A(arguments)
    for (var i=0; i<a.length; i++) {
      if (a[i].parent) a[i].removeSelf()
      this.childNodes.push(a[i])
      a[i].parent = a[i].parentNode = this
      a[i].setRoot(this.root)
    }
    this.changed = true
  },

  /**
    Removes all childNodes from the node.
    */
  removeAllChildren : function() {
    this.remove.apply(this, this.childNodes)
  },

  /**
    Alias for remove().

    @param Node[s] to remove
    */
  removeChild : function() {
    return this.remove.apply(this, arguments)
  },

  /**
    Removes arguments from the node's childNodes.

    Removing a child sets its parent to null and calls
    child.setRoot(null)

    @param Child node[s] to remove
    */
  remove : function(obj) {
    var a = arguments
    for (var i=0; i<a.length; i++) {
      this.childNodes.deleteFirst(a[i])
      delete a[i].parent
      delete a[i].parentNode
      a[i].setRoot(null)
    }
    this.changed = true
  },

  /**
    Calls this.parent.removeChild(this) if this.parent is set.
    */
  removeSelf : function() {
    if (this.parentNode) {
      this.parentNode.remove(this)
    }
  },

  /**
    Returns true if this node's subtree contains obj. (I.e. obj is this or
    obj's parent chain includes this.)

    @param obj Node to look for
    @return True if obj is in this node's subtree, false if it isn't.
    */
  contains : function(obj) {
    while (obj) {
      if (obj == this) return true
      obj = obj.parentNode
    }
    return false
  },

  /**
    Set this.root to the given value and propagate the update to childNodes.

    @param root The new root node
    @private
    */
  setRoot : function(root) {
    if (!root) root = this
    this.dispatchEvent({type: 'rootChanged', canvasTarget: this, relatedTarget: root})
    this.root = root
    for (var i=0; i<this.childNodes.length; i++)
      this.childNodes[i].setRoot(root)
  },

  /**
    Adds a callback function to be called before drawing each frame.

    @param f Callback function
    */
  addFrameListener : function(f) {
    this.frameListeners.push(f)
  },

  /**
    Removes a callback function from update callbacks.

    @param f Callback function
    */
  removeFrameListener : function(f) {
    this.frameListeners.deleteFirst(f)
  },

  addEventListener : function(type, listener, capture) {
    if (!this.eventListeners[type])
      this.eventListeners[type] = {capture:[], bubble:[]}
    this.eventListeners[type][capture ? 'capture' : 'bubble'].push(listener)
  },

  /**
    Synonym for addEventListener.
  */
  when : function(type, listener, capture) {
    this.addEventListener(type, listener, capture || false)
  },

  removeEventListener : function(type, listener, capture) {
    if (!this.eventListeners[type]) return
    this.eventListeners[type][capture ? 'capture' : 'bubble'].deleteFirst(listener)
    if (this.eventListeners[type].capture.length == 0 &&
        this.eventListeners[type].bubble.length == 0)
      delete this.eventListeners[type]
  },

  dispatchEvent : function(event) {
    var type = event.type
    if (!event.canvasTarget) {
      if (type.search(/^(key|text)/i) == 0) {
        event.canvasTarget = this.root.focused || this.root.target
      } else {
        event.canvasTarget = this.root.target
      }
      if (!event.canvasTarget)
        event.canvasTarget = this
    }
    var path = []
    var obj = event.canvasTarget
    while (obj && obj != this) {
      path.push(obj)
      obj = obj.parent
    }
    path.push(this)
    event.canvasPhase = 'capture'
    for (var i=path.length-1; i>=0; i--)
      if (!path[i].handleEvent(event)) return false
    event.canvasPhase = 'bubble'
    for (var i=0; i<path.length; i++)
      if (!path[i].handleEvent(event)) return false
    return true
  },

  broadcastEvent : function(event) {
    var type = event.type
    event.canvasPhase = 'capture'
    if (!this.handleEvent(event)) return false
    for (var i=0; i<this.childNodes.length; i++)
      if (!this.childNodes[i].broadcastEvent(event)) return false
    event.canvasPhase = 'bubble'
    if (!this.handleEvent(event)) return false
    return true
  },

  handleEvent : function(event) {
    var type = event.type
    var phase = event.canvasPhase
    if (this.cursor && phase == 'capture')
      event.cursor = this.cursor
    var els = this.eventListeners[type]
    els = els && els[phase]
    if (els) {
      for (var i=0; i<els.length; i++) {
        var rv = els[i].call(this, event)
        if (rv == false || event.stopped) {
          if (!event.stopped)
            event.stopPropagation()
          event.stopped = true
          return false
        }
      }
    }
    return true
  },

  /**
    Handle scenegraph update.
    Called with current time before drawing each frame.

    This method should be touched only if you know what you're doing.
    If you need your own update handler, either add a frame listener or
    overwrite {@link CanvasNode#update}.

    @param time Current animation time
    @param timeDelta Time since last frame in milliseconds
    */
  handleUpdate : function(time, timeDelta) {
    this.update(time, timeDelta)
    this.willBeDrawn = (!this.parent || this.parent.willBeDrawn) && (this.display ? this.display != 'none' : this.visible)
    for(var i=0; i<this.childNodes.length; i++)
      this.childNodes[i].handleUpdate(time, timeDelta)
    // TODO propagate dirty area bbox up the scene graph
    if (this.parent && this.changed) {
      this.parent.changed = this.changed
      this.changed = false
    }
    this.needMatrixUpdate = true
  },

  /**
    Update this node. Calls all frame listener callbacks in the order they
    were added.

    Overwrite this with your own method if you want to do things differently.

    @param time Current animation time
    @param timeDelta Time since last frame in milliseconds
    */
  update : function(time, timeDelta) {
    // need to operate on a copy, otherwise bad stuff happens
    var fl = this.frameListeners.slice(0)
    for(var i=0; i<fl.length; i++) {
      if (this.frameListeners.includes(fl[i]))
        fl[i].apply(this, arguments)
    }
  },

  /**
    Tests if this node or its subtree is under the mouse cursor and
    sets this.underCursor accordingly.

    If this node (and not one of its childNodes) is under the mouse cursor
    this.root.target is set to this. This way, the topmost (== drawn last)
    node under the mouse cursor is the root target.

    To see whether a subtree node is the current target:

    if (this.underCursor && this.contains(this.root.target)) {
      // we are the target, let's roll
    }

    This method should be touched only if you know what you're doing.
    Overwrite {@link CanvasNode#drawPickingPath} to change the way the node's
    picking path is created.

    Called after handleUpdate, but before handleDraw.

    @param ctx Canvas 2D context
    */
  handlePick : function(ctx) {
    // CSS display & visibility
    if (this.display)
      this.visible = (this.display != 'none')
    if (this.visibility)
      this.drawable = (this.visibility != 'hidden')
    this.underCursor = false
    if (this.visible && this.catchMouse && this.root.absoluteMouseX != null) {
      ctx.save()
      this.transform(ctx, true)
      if (this.pickable && this.drawable) {
        if (ctx.isPointInPath) {
          ctx.beginPath()
          if (this.drawPickingPath)
            this.drawPickingPath(ctx)
        }
        this.underCursor = CanvasSupport.isPointInPath(
                              this.drawPickingPath ? ctx : false,
                              this.root.mouseX,
                              this.root.mouseY,
                              this.currentMatrix,
                              this)
        if (this.underCursor)
          this.root.target = this
      } else {
        this.underCursor = false
      }
      var c = this.__getChildrenCopy()
      this.__zSort(c)
      for(var i=0; i<c.length; i++) {
        c[i].handlePick(ctx)
        if (!this.underCursor)
          this.underCursor = c[i].underCursor
      }
      ctx.restore()
    } else {
      var c = this.__getChildrenCopy()
      while (c.length > 0) {
        var c0 = c.pop()
        if (c0.underCursor) {
          c0.underCursor = false
          Array.prototype.push.apply(c, c0.childNodes)
        }
      }
    }
  },

  __zSort : function(c) {
    c.stableSort(function(c1,c2) { return c1.zIndex - c2.zIndex; });
  },

  __getChildrenCopy : function() {
    if (this.__childNodesCopy) {
      while (this.__childNodesCopy.length > this.childNodes.length)
        this.__childNodesCopy.pop()
      for (var i=0; i<this.childNodes.length; i++)
        this.__childNodesCopy[i] = this.childNodes[i]
    } else {
      this.__childNodesCopy = this.childNodes.slice(0)
    }
    return this.__childNodesCopy
  },

  /**
    Returns true if the point x,y is inside the path of a drawable node.

    The x,y point is in user-space coordinates, meaning that e.g. the point
    5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
    transform on the rectangle.

    Leave isPointInPath to false to avoid unnecessary matrix inversions for
    non-drawables.

    @param x X-coordinate of the point.
    @param y Y-coordinate of the point.
    @return Whether the point is inside the path of this node.
    @type boolean
    */
  isPointInPath : false,

  /**
    Handles transforming and drawing the node and its childNodes
    on each frame.

    Pushes context state, applies state transforms and draws the node.
    Then sorts the node's childNodes by zIndex, smallest first, and
    calls their handleDraws in that order. Finally, pops the context state.

    Called after handleUpdate and handlePick.

    This method should be touched only if you know what you're doing.
    Overwrite {@link CanvasNode#draw} when you need to draw things.

    @param ctx Canvas 2D context
    */
  handleDraw : function(ctx) {
    // CSS display & visibility
    if (this.display)
      this.visible = (this.display != 'none')
    if (this.visibility)
      this.drawable = (this.visibility != 'hidden')
    if (!this.visible) return
    ctx.save()
    var pff = ctx.fontFamily
    var pfs = ctx.fontSize
    var pfo = ctx.fillOn
    var pso = ctx.strokeOn
    if (this.fontFamily)
      ctx.fontFamily = this.fontFamily
    if (this.fontSize)
      ctx.fontSize = this.fontSize
    this.transform(ctx)
    if (this.clipPath) {
      ctx.beginPath()
      if (this.clipPath.units == this.OBJECTBOUNDINGBOX) {
        var bb = this.getSubtreeBoundingBox(true)
        ctx.save()
        ctx.translate(bb[0], bb[1])
        ctx.scale(bb[2], bb[3])
        this.clipPath.createSubtreePath(ctx, true)
        ctx.restore()
        ctx.clip()
      } else {
        this.clipPath.createSubtreePath(ctx, true)
        ctx.clip()
      }
    }
    if (this.drawable && this.draw)
      this.draw(ctx)
    var c = this.__getChildrenCopy()
    this.__zSort(c);
    for(var i=0; i<c.length; i++) {
      c[i].handleDraw(ctx)
    }
    ctx.fontFamily = pff
    ctx.fontSize = pfs
    ctx.fillOn = pfo
    ctx.strokeOn = pso
    ctx.restore()
  },

  /**
    Transforms the context state according to this node's attributes.

    @param ctx Canvas 2D context
    @param onlyTransform If set to true, only do matrix transforms.
    */
  transform : function(ctx, onlyTransform) {
    Transformable.prototype.transform.call(this, ctx)

    if (onlyTransform) return

    // stroke / fill modifiers
    if (this.fill != null) {
      if (!this.fill || this.fill == 'none') {
        ctx.fillOn = false
      } else {
        ctx.fillOn = true
        if (this.fill != true) {
          var fillStyle = Colors.parseColorStyle(this.fill, ctx)
          ctx.setFillStyle( fillStyle )
        }
      }
    }
    if (this.stroke != null) {
      if (!this.stroke || this.stroke == 'none') {
        ctx.strokeOn = false
      } else {
        ctx.strokeOn = true
        if (this.stroke != true)
          ctx.setStrokeStyle( Colors.parseColorStyle(this.stroke, ctx) )
      }
    }
    if (this.strokeWidth != null)
      ctx.setLineWidth( this.strokeWidth )
    if (this.lineCap != null)
      ctx.setLineCap( this.lineCap )
    if (this.lineJoin != null)
      ctx.setLineJoin( this.lineJoin )
    if (this.miterLimit != null)
      ctx.setMiterLimit( this.miterLimit )

    // compositing modifiers
    if (this.absoluteOpacity != null)
      ctx.setGlobalAlpha( this.absoluteOpacity )
    if (this.opacity != null)
      ctx.setGlobalAlpha( ctx.globalAlpha * this.opacity )
    if (this.compositeOperation != null)
      ctx.setGlobalCompositeOperation( this.compositeOperation )

    // shadow modifiers
    if (this.shadowColor != null)
      ctx.setShadowColor( Colors.parseColorStyle(this.shadowColor, ctx) )
    if (this.shadowBlur != null)
      ctx.setShadowBlur( this.shadowBlur )
    if (this.shadowOffsetX != null)
      ctx.setShadowOffsetX( this.shadowOffsetX )
    if (this.shadowOffsetY != null)
      ctx.setShadowOffsetY( this.shadowOffsetY )

    // text modifiers
    if (this.textAlign != null)
      ctx.setTextAlign( this.textAlign )
    if (this.textBaseline != null)
      ctx.setTextBaseline( this.textBaseline )
    if (this.font != null)
      ctx.setFont( this.font )
  },

  /**
    Draws the picking path for the node for testing if the mouse cursor
    is inside the node.

    False by default, overwrite if you need special behaviour.

    @param ctx Canvas 2D context
    */
  drawPickingPath : false,

  /**
    Draws the node.

    False by default, overwrite to actually draw something.

    @param ctx Canvas 2D context
    */
  draw : false,

  createSubtreePath : function(ctx, skipTransform) {
    ctx.save()
    if (!skipTransform) this.transform(ctx, true)
    for (var i=0; i<this.childNodes.length; i++)
      this.childNodes[i].createSubtreePath(ctx)
    ctx.restore()
  },

  getSubtreeBoundingBox : function(identity) {
    if (identity) {
      var p = this.parent
      this.parent = null
      this.needMatrixUpdate = true
    }
    var bb = this.getAxisAlignedBoundingBox()
    for (var i=0; i<this.childNodes.length; i++) {
      var cbb = this.childNodes[i].getSubtreeBoundingBox()
      if (!bb) {
        bb = cbb
      } else if (cbb) {
        this.mergeBoundingBoxes(bb, cbb)
      }
    }
    if (identity) {
      this.parent = p
      this.needMatrixUpdate = true
    }
    return bb
  },

  mergeBoundingBoxes : function(bb, bb2) {
    if (bb[0] > bb2[0]) bb[0] = bb2[0]
    if (bb[1] > bb2[1]) bb[1] = bb2[1]
    if (bb[2]+bb[0] < bb2[2]+bb2[0]) bb[2] = bb2[2]+bb2[0]-bb[0]
    if (bb[3]+bb[1] < bb2[3]+bb2[1]) bb[3] = bb2[3]+bb2[1]-bb[1]
  },

  getAxisAlignedBoundingBox : function() {
    this.transform(null, true)
    if (!this.getBoundingBox) return null
    var bbox = this.getBoundingBox()
    var xy1 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
      bbox[0], bbox[1])
    var xy2 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
      bbox[0]+bbox[2], bbox[1]+bbox[3])
    var xy3 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
      bbox[0], bbox[1]+bbox[3])
    var xy4 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
      bbox[0]+bbox[2], bbox[1])
    var x1 = Math.min(xy1[0], xy2[0], xy3[0], xy4[0])
    var x2 = Math.max(xy1[0], xy2[0], xy3[0], xy4[0])
    var y1 = Math.min(xy1[1], xy2[1], xy3[1], xy4[1])
    var y2 = Math.max(xy1[1], xy2[1], xy3[1], xy4[1])
    return [x1, y1, x2-x1, y2-y1]
  },

  makeDraggable : function() {
    this.addEventListener('dragstart', function(ev) {
      this.dragStartPosition = {x: this.x, y: this.y};
      ev.stopPropagation();
      ev.preventDefault();
      return false;
    }, false);
    this.addEventListener('drag', function(ev) {
      this.x = this.dragStartPosition.x + this.root.dragX / this.parent.currentMatrix[0];
      this.y = this.dragStartPosition.y + this.root.dragY / this.parent.currentMatrix[3];
      ev.stopPropagation();
      ev.preventDefault();
      return false;
    }, false);
  }
})


/**
  Canvas is the canvas manager class.
  It takes care of updating and drawing its childNodes on a canvas element.

  An example with a rotating rectangle:

    var c = E.canvas(500, 500)
    var canvas = new Canvas(c)
    var rect = new Rectangle(100, 100)
    rect.x = 250
    rect.y = 250
    rect.fill = true
    rect.fillStyle = 'green'
    rect.addFrameListener(function(t) {
      this.rotation = ((t / 3000) % 1) * Math.PI * 2
    })
    canvas.append(rect)
    document.body.appendChild(c)


  To use the canvas as a manually updated image:

    var canvas = new Canvas(E.canvas(200,40), {
      isPlaying : false,
      redrawOnlyWhenChanged : true
    })
    var c = new Circle(20)
    c.x = 100
    c.y = 20
    c.fill = true
    c.fillStyle = 'red'
    c.addFrameListener(function(t) {
      if (this.root.absoluteMouseX != null) {
        this.x = this.root.mouseX // relative to canvas surface
        this.root.changed = true
      }
    })
    canvas.append(c)


  Or by using raw onFrame-calls:

    var canvas = new Canvas(E.canvas(200,40), {
      isPlaying : false,
      fill : true,
      fillStyle : 'white'
    })
    var c = new Circle(20)
    c.x = 100
    c.y = 20
    c.fill = true
    c.fillStyle = 'red'
    canvas.append(c)
    canvas.onFrame()


  Which is also the recommended way to use a canvas inside another canvas:

    var canvas = new Canvas(E.canvas(200,40), {
      isPlaying : false
    })
    var c = new Circle(20, {
      x: 100, y: 20,
      fill: true, fillStyle: 'red'
    })
    canvas.append(c)

    var topCanvas = new Canvas(E.canvas(500, 500))
    var canvasImage = new ImageNode(canvas.canvas, {x: 250, y: 250})
    topCanvas.append(canvasImage)
    canvasImage.addFrameListener(function(t) {
      this.rotation = (t / 3000 % 1) * Math.PI * 2
      canvas.onFrame(t)
    })

  */
Canvas = Klass(CanvasNode, {

  clear : true,
  frameLoop : false,
  recording : false,
  opacity : 1,
  frame : 0,
  elapsed : 0,
  frameDuration : 30,
  speed : 1.0,
  time : 0,
  fps : 0,
  currentRealFps : 0,
  currentFps : 0,
  fpsFrames : 30,
  startTime : 0,
  realFps : 0,
  fixedTimestep : false,
  playOnlyWhenFocused : true,
  isPlaying : true,
  redrawOnlyWhenChanged : false,
  changed : true,
  drawBoundingBoxes : false,
  cursor : 'default',

  mouseDown : false,
  mouseEvents : [],

  // absolute pixel coordinates from canvas top-left
  absoluteMouseX : null,
  absoluteMouseY : null,

  /*
    Coordinates relative to the canvas's surface scale.
    Example:
      canvas.width
      #=> 100
      canvas.style.width
      #=> '100px'
      canvas.absoluteMouseX
      #=> 50
      canvas.mouseX
      #=> 50

      canvas.style.width = '200px'
      canvas.width
      #=> 100
      canvas.absoluteMouseX
      #=> 100
      canvas.mouseX
      #=> 50
  */
  mouseX : null,
  mouseY : null,

  elementNodeZIndexCounter : 0,

  initialize : function(canvas, config) {
    if (arguments.length > 2) {
      var container = arguments[0]
      var w = arguments[1]
      var h = arguments[2]
      var config = arguments[3]
      var canvas = E.canvas(w,h)
      var canvasContainer = E('div', canvas, {style:
        {overflow:'hidden', width:w+'px', height:h+'px', position:'relative'}
      })
      this.canvasContainer = canvasContainer
      if (container)
        container.appendChild(canvasContainer)
    }
    CanvasNode.initialize.call(this, config)
    this.mouseEventStack = []
    this.canvas = canvas
    canvas.canvas = this
    this.width = this.canvas.width
    this.height = this.canvas.height
    var th = this
    this.frameHandler = function() { th.onFrame() }
    this.canvas.addEventListener('DOMNodeInserted', function(ev) {
      if (ev.target == this)
        th.addEventListeners()
    }, false)
    this.canvas.addEventListener('DOMNodeRemoved', function(ev) {
      if (ev.target == this)
        th.removeEventListeners()
    }, false)
    if (this.canvas.parentNode) this.addEventListeners()
    this.startTime = new Date().getTime()
    if (this.isPlaying)
      this.play()
  },

  // FIXME
  removeEventListeners : function() {
  },

  addEventListeners : function() {
    var th = this
    this.canvas.parentNode.addMouseEvent = function(e){
      var xy = Mouse.getRelativeCoords(this, e)
      th.absoluteMouseX = xy.x
      th.absoluteMouseY = xy.y
      var style = document.defaultView.getComputedStyle(th.canvas,"")
      var w = parseFloat(style.getPropertyValue('width'))
      var h = parseFloat(style.getPropertyValue('height'))
      th.mouseX = th.absoluteMouseX * (w / th.canvas.width)
      th.mouseY = th.absoluteMouseY * (h / th.canvas.height)
      th.addMouseEvent(th.mouseX, th.mouseY, th.mouseDown)
    }
    this.canvas.parentNode.contains = this.contains

    this.canvas.parentNode.addEventListener('mousedown', function(e) {
      th.mouseDown = true
      if (th.keyTarget != th.target) {
        if (th.keyTarget)
          th.dispatchEvent({type: 'blur', canvasTarget: th.keyTarget})
        th.keyTarget = th.target
        if (th.keyTarget)
          th.dispatchEvent({type: 'focus', canvasTarget: th.keyTarget})
      }
      this.addMouseEvent(e)
    }, true)

    this.canvas.parentNode.addEventListener('mouseup', function(e) {
      this.addMouseEvent(e)
      th.mouseDown = false
    }, true)

    this.canvas.parentNode.addEventListener('mousemove', function(e) {
      this.addMouseEvent(e)
      if (th.prevClientX == null) {
        th.prevClientX = e.clientX
        th.prevClientY = e.clientY
      }
      if (th.dragTarget) {
        var nev = document.createEvent('MouseEvents')
        nev.initMouseEvent('drag', true, true, window, e.detail,
          e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
          e.shiftKey, e.metaKey, e.button, e.relatedTarget)
        nev.canvasTarget = th.dragTarget
        nev.dx = e.clientX - th.prevClientX
        nev.dy = e.clientY - th.prevClientY
        th.dragX += nev.dx
        th.dragY += nev.dy
        th.dispatchEvent(nev)
      }
      if (!th.mouseDown) {
        if (th.dragTarget) {
          var nev = document.createEvent('MouseEvents')
          nev.initMouseEvent('dragend', true, true, window, e.detail,
            e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
            e.shiftKey, e.metaKey, e.button, e.relatedTarget)
          nev.canvasTarget = th.dragTarget
          th.dispatchEvent(nev)
          th.dragX = th.dragY = 0
          th.dragTarget = false
        }
      } else if (!th.dragTarget && th.target) {
        th.dragTarget = th.target
        var nev = document.createEvent('MouseEvents')
        nev.initMouseEvent('dragstart', true, true, window, e.detail,
          e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
          e.shiftKey, e.metaKey, e.button, e.relatedTarget)
        nev.canvasTarget = th.dragTarget
        th.dragStartX = e.clientX
        th.dragStartY = e.clientY
        th.dragX = th.dragY = 0
        th.dispatchEvent(nev)
      }
      th.prevClientX = e.clientX
      th.prevClientY = e.clientY
    }, true)

    this.canvas.parentNode.addEventListener('mouseout', function(e) {
      if (!CanvasNode.contains.call(this, e.relatedTarget))
        th.absoluteMouseX = th.absoluteMouseY = th.mouseX = th.mouseY = null
    }, true)

    var dispatch = this.dispatchEvent.bind(this)
    var types = [
      'mousemove', 'mouseover', 'mouseout',
      'click', 'dblclick',
      'mousedown', 'mouseup',
      'keypress', 'keydown', 'keyup',
      'DOMMouseScroll', 'mousewheel', 'mousemultiwheel', 'textInput',
      'focus', 'blur'
    ]
    for (var i=0; i<types.length; i++) {
      this.canvas.parentNode.addEventListener(types[i], dispatch, false)
    }
    this.keys = {}

    this.windowEventListeners = {

      keydown : function(ev) {
        if (th.keyTarget) {
          th.updateKeys(ev)
          ev.canvasTarget = th.keyTarget
          th.dispatchEvent(ev)
        }
      },

      keyup : function(ev) {
        if (th.keyTarget) {
          th.updateKeys(ev)
          ev.canvasTarget = th.keyTarget
          th.dispatchEvent(ev)
        }
      },

      // do we even want to have this?
      keypress : function(ev) {
        if (th.keyTarget) {
          ev.canvasTarget = th.keyTarget
          th.dispatchEvent(ev)
        }
      },

      blur : function(ev) {
        th.absoluteMouseX = th.absoluteMouseY = null
        if (th.playOnlyWhenFocused && th.isPlaying) {
          th.stop()
          th.__blurStop = true
        }
      },

      focus : function(ev) {
        if (th.__blurStop && !th.isPlaying) th.play()
      },

      mouseup : function(e) {
        th.mouseDown = false
        if (th.dragTarget) {
          // TODO
          // find the object that receives the drag (i.e. drop target)
          var nev = document.createEvent('MouseEvents')
          nev.initMouseEvent('dragend', true, true, window, e.detail,
            e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
            e.shiftKey, e.metaKey, e.button, e.relatedTarget)
          nev.canvasTarget = th.dragTarget
          th.dispatchEvent(nev)
          th.dragTarget = false
        }
        if (!th.canvas.parentNode.contains(e.target)) {
          var rv = th.dispatchEvent(e)
          if (th.keyTarget) {
            th.dispatchEvent({type: 'blur', canvasTarget: th.keyTarget})
            th.keyTarget = null
          }
          return rv
        }
      },

      mousemove : function(ev) {
        if (th.__blurStop && !th.isPlaying) th.play()
        if (!th.canvas.parentNode.contains(ev.target) && th.mouseDown)
          return th.dispatchEvent(ev)
      }

    }

    this.canvas.parentNode.addEventListener('DOMNodeRemoved', function(ev) {
      if (ev.target == this)
        th.removeWindowEventListeners()
    }, false)
    this.canvas.parentNode.addEventListener('DOMNodeInserted', function(ev) {
      if (ev.target == this)
        th.addWindowEventListeners()
    }, false)
    if (this.canvas.parentNode.parentNode) this.addWindowEventListeners()
  },

  updateKeys : function(ev) {
    this.keys.shift = ev.shiftKey
    this.keys.ctrl = ev.ctrlKey
    this.keys.alt = ev.altKey
    this.keys.meta = ev.metaKey
    var state = (ev.type == 'keydown')
    switch (ev.keyCode) {
      case 37: this.keys.left = state; break
      case 38: this.keys.up = state; break
      case 39: this.keys.right = state; break
      case 40: this.keys.down = state; break
      case 32: this.keys.space = state; break
      case 13: this.keys.enter = state; break
      case 9: this.keys.tab = state; break
      case 8: this.keys.backspace = state; break
      case 16: this.keys.shift = state; break
      case 17: this.keys.ctrl = state; break
      case 18: this.keys.alt = state; break
    }
    this.keys[ev.keyCode] = state
  },

  addWindowEventListeners : function() {
    for (var i in this.windowEventListeners)
      window.addEventListener(i, this.windowEventListeners[i], false)
  },

  removeWindowEventListeners : function() {
    for (var i in this.windowEventListeners)
      window.removeEventListener(i, this.windowEventListeners[i], false)
  },

  addMouseEvent : function(x,y,mouseDown) {
    var a = this.allocMouseEvent()
    a[0] = x
    a[1] = y
    a[2] = mouseDown
    this.mouseEvents.push(a)
  },

  allocMouseEvent : function() {
    if (this.mouseEventStack.length > 0) {
      return this.mouseEventStack.pop()
    } else {
      return [null, null, null]
    }
  },

  freeMouseEvent : function(ev) {
    this.mouseEventStack.push(ev)
    if (this.mouseEventStack.length > 100)
      this.mouseEventStack.splice(0,this.mouseEventStack.length)
  },

  clearMouseEvents : function() {
    while (this.mouseEvents.length > 0)
      this.freeMouseEvent(this.mouseEvents.pop())
  },

  /**
    Start frame loop.

    The frame loop is an interval, where #onFrame is called every
    #frameDuration milliseconds.
    */
  play : function() {
    this.stop()
    this.realTime = new Date().getTime()
    this.frameLoop = setInterval(this.frameHandler, this.frameDuration)
    this.isPlaying = true
  },

  /**
    Stop frame loop.
    */
  stop : function() {
    this.__blurStop = false
    if (this.frameLoop) {
      clearInterval(this.frameLoop)
      this.frameLoop = false
    }
    this.isPlaying = false
  },

  dispatchEvent : function(ev) {
    var rv = CanvasNode.prototype.dispatchEvent.call(this, ev)
    if (ev.cursor) {
      if (this.canvas.style.cursor != ev.cursor)
        this.canvas.style.cursor = ev.cursor
    } else {
      if (this.canvas.style.cursor != this.cursor)
        this.canvas.style.cursor = this.cursor
    }
    return rv
  },

  /**
    The frame loop function. Called every #frameDuration milliseconds.
    Takes an optional external time parameter (for syncing Canvases with each
    other, e.g. when using a Canvas as an image.)

    If the time parameter is given, the second parameter is used as the frame
    time delta (i.e. the time elapsed since last frame.)

    If time or timeDelta is not given, the canvas computes its own timeDelta.

    @param time The external time. Optional.
    @param timeDelta Time since last frame in milliseconds. Optional.
    */
  onFrame : function(time, timeDelta) {
    this.elementNodeZIndexCounter = 0
    var ctx = this.getContext()
    try {
      var realTime = new Date().getTime()
      this.currentRealElapsed = (realTime - this.realTime)
      this.currentRealFps = 1000 / this.currentRealElapsed
      var dt = this.frameDuration * this.speed
      if (!this.fixedTimestep)
        dt = this.currentRealElapsed * this.speed
      this.realTime = realTime
      if (time != null) {
        this.time = time
        if (timeDelta)
          dt = timeDelta
      } else {
        this.time += dt
      }
      this.previousTarget = this.target
      this.target = null
      if (this.catchMouse)
        this.handlePick(ctx)
      if (this.previousTarget != this.target) {
        if (this.previousTarget) {
          var nev = document.createEvent('MouseEvents')
          nev.initMouseEvent('mouseout', true, true, window,
            0, 0, 0, 0, 0, false, false, false, false, 0, null)
          nev.canvasTarget = this.previousTarget
          this.dispatchEvent(nev)
        }
        if (this.target) {
          var nev = document.createEvent('MouseEvents')
          nev.initMouseEvent('mouseover', true, true, window,
            0, 0, 0, 0, 0, false, false, false, false, 0, null)
          nev.canvasTarget = this.target
          this.dispatchEvent(nev)
        }
      }
      this.handleUpdate(this.time, dt)
      this.clearMouseEvents()
      if (!this.redrawOnlyWhenChanged || this.changed) {
        try {
          this.handleDraw(ctx)
        } catch(e) {
          console.log(e)
          throw(e)
        }
        this.changed = false
      }
      this.currentElapsed = (new Date().getTime() - this.realTime)
      this.elapsed += this.currentElapsed
      this.currentFps = 1000 / this.currentElapsed
      this.frame++
      if (this.frame % this.fpsFrames == 0) {
        this.fps = this.fpsFrames*1000 / (this.elapsed)
        this.realFps = this.fpsFrames*1000 / (new Date().getTime() - this.startTime)
        this.elapsed = 0
        this.startTime = new Date().getTime()
      }
    } catch(e) {
      if (ctx) {
        // screwed up, context is borked
        try {
          // FIXME don't be stupid
          for (var i=0; i<1000; i++)
            ctx.restore()
        } catch(er) {}
      }
      delete this.context
      throw(e)
    }
  },

  /**
    Returns the canvas drawing context object.

    @return Canvas drawing context
    */
  getContext : function() {
    if (this.recording)
      return this.getRecordingContext()
    else if (this.useMockContext)
      return this.getMockContext()
    else
      return this.get2DContext()
  },

  /**
    Gets and returns an augmented canvas 2D drawing context.

    The canvas 2D context is augmented by setter functions for all
    its instance variables, making it easier to record canvas operations in
    a cross-browser fashion.
    */
  get2DContext : function() {
    if (!this.context) {
      var ctx = CanvasSupport.getContext(this.canvas, '2d')
      this.context = ctx
    }
    return this.context
  },

  /**
    Creates and returns a mock drawing context.

    @return Mock drawing context
    */
  getMockContext : function() {
    if (!this.fakeContext) {
      var ctx = this.get2DContext()
      this.fakeContext = {}
      var f = function(){ return this }
      for (var i in ctx) {
        if (typeof(ctx[i]) == 'function')
          this.fakeContext[i] = f
        else
          this.fakeContext[i] = ctx[i]
      }
      this.fakeContext.isMockObject = true
      this.fakeContext.addColorStop = f
    }
    return this.fakeContext
  },

  getRecordingContext : function() {
    if (!this.recordingContext)
      this.recordingContext = new RecordingContext()
    return this.recordingContext
  },

  /**
    Canvas drawPickingPath uses the canvas rectangle as its path.

    @param ctx Canvas drawing context
    */
  drawPickingPath : function(ctx) {
    ctx.rect(0,0, this.canvas.width, this.canvas.height)
  },

  isPointInPath : function(x,y) {
    return ((x >= 0) && (x <= this.canvas.width) && (y >= 0) && (y <= this.canvas.height))
  },

  /**
    Sets globalAlpha to this.opacity and clears the canvas if #clear is set to
    true. If #fill is also set to true, fills the canvas rectangle instead of
    clearing (using #fillStyle as the color.)

    @param ctx Canvas drawing context
    */
  draw : function(ctx) {
    ctx.setGlobalAlpha( this.opacity )
    if (this.clear) {
      if (ctx.fillOn) {
        ctx.beginPath()
        ctx.rect(0,0, this.canvas.width, this.canvas.height)
        ctx.fill()
      } else {
        ctx.clearRect(0,0, this.canvas.width, this.canvas.height)
      }
    }
    // set default fill and stroke for the canvas contents
    ctx.fillStyle = 'black'
    ctx.strokeStyle = 'black'
    ctx.fillOn = false
    ctx.strokeOn = false
  }
})


/**
  Hacky link class for emulating <a>.

  The correct way would be to have a real <a> under the cursor while hovering
  this, or an imagemap polygon built from the clipped subtree path.

  @param href Link href.
  @param target Link target, defaults to _self.
  @param config Optional config hash.
  */
LinkNode = Klass(CanvasNode, {
  href : null,
  target : '_self',
  cursor : 'pointer',

  initialize : function(href, target, config) {
    this.href = href
    if (target)
      this.target = target
    CanvasNode.initialize.call(this, config)
    this.setupLinkEventListeners()
  },

  setupLinkEventListeners : function() {
    this.addEventListener('click', function(ev) {
      if (ev.button == Mouse.RIGHT) return
      var target = this.target
      if ((ev.ctrlKey || ev.button == Mouse.MIDDLE) && target == '_self')
        target = '_blank'
      window.open(this.href, target)
    }, false)
  }
})


/**
  AudioNode is a CanvasNode used to play a sound.

  */
AudioNode = Klass(CanvasNode, {
  ready : false,
  autoPlay : false,
  playing : false,
  paused : false,
  pan : 0,
  volume : 1,
  loop : false,

  transformSound : false,

  initialize : function(filename, params) {
    CanvasNode.initialize.call(this, params)
    this.filename = filename
    this.when('load', this._autoPlaySound)
    this.loadSound()
  },

  loadSound : function() {
    this.sound = CanvasSupport.getSoundObject()
    if (!this.sound) return
    var self = this
    this.sound.onready = function() {
      self.ready = true
      self.root.dispatchEvent({type: 'ready', canvasTarget: self})
    }
    this.sound.onload = function() {
      self.loaded = true
      self.root.dispatchEvent({type: 'load', canvasTarget: self})
    }
    this.sound.onerror = function() {
      self.root.dispatchEvent({type: 'error', canvasTarget: self})
    }
    this.sound.onfinish = function() {
      if (self.loop) self.play()
      else self.stop()
    }
    this.sound.load(this.filename)
  },

  play : function() {
    this.playing = true
    this.needPlayUpdate = true
  },

  stop : function() {
    this.playing = false
    this.needPlayUpdate = true
  },

  pause : function() {
    if (this.needPauseUpdate) {
      this.needPauseUpdate = false
      return
    }
    this.paused = !this.paused
    this.needPauseUpdate = true
  },

  setVolume : function(v) {
    this.volume = v
    this.needStatusUpdate = true
  },

  setPan : function(p) {
    this.pan = p
    this.needStatusUpdate = true
  },

  handleUpdate : function() {
    CanvasNode.handleUpdate.apply(this, arguments)
    if (this.willBeDrawn) {
      this.transform(null, true)
      if (!this.sound) this.loadSound()
      if (this.ready) {
        if (this.transformSound) {
          var x = this.currentMatrix[4]
          var y = this.currentMatrix[5]
          var a = this.currentMatrix[2]
          var b = this.currentMatrix[3]
          var c = this.currentMatrix[0]
          var d = this.currentMatrix[1]
          var hw = this.root.width * 0.5
          var ys = Math.sqrt(a*a + b*b)
          var xs = Math.sqrt(c*c + d*d)
          this.setVolume(ys)
          this.setPan((x - hw) / hw)
        }
        if (this.needPauseUpdate) {
          this.needPauseUpdate = false
          this._pauseSound()
        }
        if (this.needPlayUpdate) {
          this.needPlayUpdate = false
          if (this.playing) this._playSound()
          else this._stopSound()
        }
        if (this.needStatusUpdate) {
          this._setSoundVolume()
          this._setSoundPan()
        }
      }
    }
  },

  _autoPlaySound : function() {
    if (this.autoPlay) this.play()
  },

  _setSoundVolume : function() {
    this.sound.setVolume(this.volume)
  },

  _setSoundPan : function() {
    this.sound.setPan(this.pan)
  },

  _playSound : function() {
    if (this.sound.play() == false)
      return this.playing = false
    this.root.dispatchEvent({type: 'play', canvasTarget: this})
  },

  _stopSound : function() {
    this.sound.stop()
    this.root.dispatchEvent({type: 'stop', canvasTarget: this})
  },

  _pauseSound : function() {
    this.sound.pause()
    this.root.dispatchEvent({type: this.paused ? 'pause' : 'play', canvasTarget: this})
  }
})


/**
  ElementNode is a CanvasNode that has an HTML element as its content.

  The content is added to an absolutely positioned HTML element, which is added
  to the root node's canvases parentNode. The content element follows the
  current transformation matrix.

  The opacity of the element is set to the globalAlpha of the drawing context
  unless #noAlpha is true.

  The font-size of the element is set to the current y-scale unless #noScaling
  is true.

  Use ElementNode when you need accessible web content in your animations.

    var e = new ElementNode(
      E('h1', 'HERZLICH WILLKOMMEN IM BAHNHOF'),
      {
        x : 40,
        y : 30
      }
    )
    e.addFrameListener(function(t) {
      this.scale = 1 + 0.5*Math.cos(t/1000)
    })

  @param content An HTML element or string of HTML to use as the content.
  @param config Optional config has.
  */
ElementNode = Klass(CanvasNode, {
  noScaling : false,
  noAlpha : false,
  inherit : 'inherit',
  align: null, // left | center | right
  valign: null, // top | center | bottom
  xOffset: 0,
  yOffset: 0,

  initialize : function(content, config) {
    CanvasNode.initialize.call(this, config)
    this.content = content
    this.element = E('div', content)
    this.element.style.MozTransformOrigin =
    this.element.style.webkitTransformOrigin = '0 0'
    this.element.style.position = 'absolute'
  },

  clone : function() {
    var c = CanvasNode.prototype.clone.call(this)
    if (this.content && this.content.cloneNode)
      c.content = this.content.cloneNode(true)
    c.element = E('div', c.content)
    c.element.style.position = 'absolute'
    c.element.style.MozTransformOrigin =
    c.element.style.webkitTransformOrigin = '0 0'
    return c
  },

  setRoot : function(root) {
    CanvasNode.setRoot.call(this, root)
    if (this.element && this.element.parentNode && this.element.parentNode.removeChild)
      this.element.parentNode.removeChild(this.element)
  },

  handleUpdate : function(t, dt) {
    CanvasNode.handleUpdate.call(this, t, dt)
    if (!this.willBeDrawn || !this.visible || this.display == 'none' || this.visibility == 'hidden' || !this.drawable) {
      if (this.element.style.display != 'none')
        this.element.style.display = 'none'
    } else if (this.element.style.display == 'none') {
      this.element.style.display = 'block'
    }
  },

  addEventListener : function(event, callback, capture) {
    var th = this
    var ccallback = function() { callback.apply(th, arguments) }
    return this.element.addEventListener(event, ccallback, capture||false)
  },

  removeEventListener : function(event, callback, capture) {
    var th = this
    var ccallback = function() { callback.apply(th, arguments) }
    return this.element.removeEventListener(event, ccallback, capture||false)
  },

  draw : function(ctx) {
    if (this.cursor && this.element.style.cursor != this.cursor)
      this.element.style.cursor = this.cursor
    if (this.element.style.zIndex != this.root.elementNodeZIndexCounter)
      this.element.style.zIndex = this.root.elementNodeZIndexCounter
    this.root.elementNodeZIndexCounter++
    var baseTransform = this.currentMatrix
    xo = this.xOffset
    yo = this.yOffset
    if (this.fillBoundingBox && this.parent && this.parent.getBoundingBox) {
      var bb = this.parent.getBoundingBox()
      xo += bb[0]
      yo += bb[1]
    }
    var xy = CanvasSupport.tMatrixMultiplyPoint(baseTransform.slice(0,4).concat([0,0]),
      xo, yo)
    var x = this.currentMatrix[4] + xy[0]
    var y = this.currentMatrix[5] + xy[1]
    var a = this.currentMatrix[2]
    var b = this.currentMatrix[3]
    var c = this.currentMatrix[0]
    var d = this.currentMatrix[1]
    var ys = Math.sqrt(a*a + b*b)
    var xs = Math.sqrt(c*c + d*d)
    if (ctx.fontFamily != null)
      this.element.style.fontFamily = ctx.fontFamily

    var wkt = CanvasSupport.isCSSTransformSupported()
    if (wkt && !this.noScaling) {
      this.element.style.MozTransform =
      this.element.style.webkitTransform = 'matrix('+baseTransform.join(",")+')'
    } else {
      this.element.style.MozTransform =
      this.element.style.webkitTransform = ''
    }
    if (ctx.fontSize != null) {
      if (this.noScaling || wkt) {
        this.element.style.fontSize = ctx.fontSize + 'px'
      } else {
        this.element.style.fontSize = ctx.fontSize * ys + 'px'
      }
    } else {
      if (this.noScaling || wkt) {
        this.element.style.fontSize = 'inherit'
      } else {
        this.element.style.fontSize = 100 * ys + '%'
      }
    }
    if (this.noAlpha)
      this.element.style.opacity = 1
    else
      this.element.style.opacity = ctx.globalAlpha
    if (!this.element.parentNode && this.root.canvas.parentNode) {
      this.element.style.visibility = 'hidden'
      this.root.canvas.parentNode.appendChild(this.element)
      var hidden = true
    }
    var fs = this.color || this.fill
    if (this.parent) {
      if (!fs || !fs.length)
        fs = this.parent.color
      if (!fs || !fs.length)
        fs = this.parent.fill
    }
    if (!fs || !fs.length)
      fs = ctx.fillStyle
    if (typeof(fs) == 'string') {
      if (fs.search(/^rgba\(/) != -1) {
        this.element.style.color = 'rgb(' +
          fs.match(/\d+/g).slice(0,3).join(",") +
          ')'
      } else {
        this.element.style.color = fs
      }
    } else if (fs.length) {
      this.element.style.color = 'rgb(' + fs.slice(0,3).map(Math.floor).join(",") + ')'
    }
    var dx = 0, dy = 0
    if (bb) {
      this.element.style.width = Math.floor(xs * bb[2]) + 'px'
      this.element.style.height = Math.floor(ys * bb[3]) + 'px'
      this.eWidth = xs
      this.eHeight = ys
    } else {
      this.element.style.width = ''
      this.element.style.height = ''
      var align = this.align || this.textAnchor
      var origin = [0,0]
      if (align == 'center' || align == 'middle') {
        dx = -this.element.offsetWidth / 2
        origin[0] = '50%'
      } else if (align == 'right') {
        dx = -this.element.offsetWidth
        origin[0] = '100%'
      }
      var valign = this.valign
      if (valign == 'center' || valign == 'middle') {
        dy = -this.element.offsetHeight / 2
        origin[1] = '50%'
      } else if (valign == 'bottom') {
        dy = -this.element.offsetHeight
        origin[1] = '100%'
      }
      this.element.style.webkitTransformOrigin =
      this.element.style.MozTransformOrigin = origin.join(" ")
      this.eWidth = this.element.offsetWidth / xs
      this.eHeight = this.element.offsetHeight / ys
    }
    if (wkt && !this.noScaling) {
      this.element.style.left = Math.floor(dx) + 'px'
      this.element.style.top = Math.floor(dy) + 'px'
    } else {
      this.element.style.left = Math.floor(x+dx) + 'px'
      this.element.style.top = Math.floor(y+dy) + 'px'
    }
    if (hidden)
      this.element.style.visibility = 'visible'
  }
})


/**
  A Drawable is a CanvasNode with possible fill, stroke and clip.

  It draws the path by calling #drawGeometry
  */
Drawable = Klass(CanvasNode, {
  pickable : true,
  //   'inside' // clip before drawing the stroke
  // | 'above'  // draw stroke after the fill
  // | 'below'  // draw stroke before the fill
  strokeMode : 'above',

  ABOVE : 'above', BELOW : 'below', INSIDE : 'inside',

  initialize : function(config) {
    CanvasNode.initialize.call(this, config)
  },

  /**
    Draws the picking path for the Drawable.

    The default version begins a new path and calls drawGeometry.

    @param ctx Canvas drawing context
    */
  drawPickingPath : function(ctx) {
    if (!this.drawGeometry) return
    ctx.beginPath()
    this.drawGeometry(ctx)
  },

  /**
    Returns true if the point x,y is inside the path of a drawable node.

    The x,y point is in user-space coordinates, meaning that e.g. the point
    5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
    transform on the rectangle.

    @param x X-coordinate of the point.
    @param y Y-coordinate of the point.
    @return Whether the point is inside the path of this node.
    @type boolean
    */
  isPointInPath : function(x, y) {
    return false
  },

  isVisible : function(ctx) {
    var abb = this.getAxisAlignedBoundingBox()
    if (!abb) return true
    var x1 = abb[0], x2 = abb[0]+abb[2], y1 = abb[1], y2 = abb[1]+abb[3]
    var w = this.root.width
    var h = this.root.height
    if (this.root.drawBoundingBoxes) {
      ctx.save()
        var bbox = this.getBoundingBox()
        ctx.beginPath()
        ctx.rect(bbox[0], bbox[1], bbox[2], bbox[3])
        ctx.strokeStyle = 'green'
        ctx.lineWidth = 1
        ctx.stroke()
      ctx.restore()
      ctx.save()
        CanvasSupport.setTransform(ctx, [1,0,0,1,0,0], this.currentMatrix)
        ctx.beginPath()
        ctx.rect(x1, y1, x2-x1, y2-y1)
        ctx.strokeStyle = 'red'
        ctx.lineWidth = 1.5
        ctx.stroke()
      ctx.restore()
    }
    var visible = !(x2 < 0 || x1 > w || y2 < 0 || y1 > h)
    return visible
  },

  createSubtreePath : function(ctx, skipTransform) {
    ctx.save()
    if (!skipTransform) this.transform(ctx, true)
    if (this.drawGeometry) this.drawGeometry(ctx)
    for (var i=0; i<this.childNodes.length; i++)
      this.childNodes[i].createSubtreePath(ctx)
    ctx.restore()
  },

  /**
    Draws the Drawable. Begins a path and calls this.drawGeometry, followed by
    possibly filling, stroking and clipping the path, depending on whether
    #fill, #stroke and #clip are set.

    @param ctx Canvas drawing context
    */
  draw : function(ctx) {
    if (!this.drawGeometry) return
    // bbox checking is slower than just drawing in most cases.
    // and caching the bboxes is hard to do correctly.
    // plus, bboxes aren't hierarchical.
    // so we are being glib :|
    if (this.root.drawBoundingBoxes)
      this.isVisible(ctx)
    var ft = (ctx.fillStyle.transformList ||
              ctx.fillStyle.matrix ||
              ctx.fillStyle.scale != null ||
              ctx.fillStyle.rotation ||
              ctx.fillStyle.x ||
              ctx.fillStyle.y )
    var st = (ctx.strokeStyle.transformList ||
              ctx.strokeStyle.matrix ||
              ctx.strokeStyle.scale != null ||
              ctx.strokeStyle.rotation ||
              ctx.strokeStyle.x ||
              ctx.strokeStyle.y )
    ctx.beginPath()
    this.drawGeometry(ctx)
    if (ctx.strokeOn) {
      switch (this.strokeMode) {
        case this.ABOVE:
          if (ctx.fillOn) this.doFill(ctx,ft)
          this.doStroke(ctx, st)
          break
        case this.BELOW:
          this.doStroke(ctx, st)
          if (ctx.fillOn) this.doFill(ctx,ft)
          break
        case this.INSIDE:
          if (ctx.fillOn) this.doFill(ctx,ft)
          ctx.save()
          var lw = ctx.lineWidth
          ctx.setLineWidth(1)
          this.doStroke(ctx, st)
          ctx.setLineWidth(lw)
          ctx.clip()
          this.doStroke(ctx, st)
          ctx.restore()
          break
      }
    } else if (ctx.fillOn) {
      this.doFill(ctx,ft)
    }
    this.drawMarkers(ctx)
    if (this.clip) ctx.clip()
  },

  doFill : function(ctx, ft) {
    if (ft || (this.getBoundingBox && ctx.fillStyle.units == this.OBJECTBOUNDINGBOX)) {
      ctx.save()
      if (this.getBoundingBox && ctx.fillStyle.units == this.OBJECTBOUNDINGBOX) {
        var bb = this.getBoundingBox()
        var sx = bb[2]
        var sy = bb[3]
        ctx.translate(bb[0],bb[1])
        ctx.scale(sx,sy)
      }
      ctx.fillStyle.transform(ctx)
    }
    if (this.fillOpacity != null) {
      var go = ctx.globalAlpha
      ctx.setGlobalAlpha(go * this.fillOpacity)
      ctx.fill()
      ctx.globalAlpha = go
    } else {
      ctx.fill()
    }
    if (ft) ctx.restore()
  },

  doStroke : function(ctx, st) {
    if (st || (this.getBoundingBox && ctx.strokeStyle.units == this.OBJECTBOUNDINGBOX)) {
      ctx.save()
      if (this.getBoundingBox && ctx.strokeStyle.units == this.OBJECTBOUNDINGBOX) {
        var bb = this.getBoundingBox()
        var sx = bb[2]
        var sy = bb[3]
        ctx.translate(bb[0],bb[1])
        ctx.scale(sx,sy)
      }
      ctx.strokeStyle.needMatrixUpdate = true
      ctx.strokeStyle.transform(ctx)
      if (sx != null)
        CanvasSupport.tScale(ctx.strokeStyle.currentMatrix, sx, sy)
      var cm = ctx.strokeStyle.currentMatrix
      // fix stroke width scale (non-uniform scales screw us up though)
      var sw = Math.sqrt(Math.max(
        cm[0]*cm[0] + cm[1]*cm[1],
        cm[2]*cm[2] + cm[3]*cm[3]
      ))
      ctx.setLineWidth(((ctx.lineWidth == null) ? 1 : ctx.lineWidth) / sw)
    }
    if (this.strokeOpacity != null) {
      var go = ctx.globalAlpha
      ctx.setGlobalAlpha(go * this.strokeOpacity)
      ctx.stroke()
      ctx.globalAlpha = go
    } else {
      ctx.stroke()
    }
    if (st) ctx.restore()
  },

  drawMarkers : function(ctx) {
    var sm = this.markerStart || this.marker
    var em = this.markerEnd || this.marker
    var mm = this.markerMid || this.marker
    if (sm && this.getStartPoint) {
      var pa = this.getStartPoint()
      if (sm.orient != null && sm.orient != 'auto')
        pa.angle = sm.orient
      var scale = (sm.markerUnits == 'strokeWidth') ? ctx.lineWidth : 1
      ctx.save()
        ctx.translate(pa.point[0], pa.point[1])
        ctx.scale(scale, scale)
        ctx.rotate(pa.angle)
        var mat = CanvasSupport.tRotate(
          CanvasSupport.tScale(
          CanvasSupport.tTranslate(
            this.currentMatrix.slice(0),
            pa.point[0], pa.point[1]
          ), scale, scale), pa.angle)
        sm.__copyMatrix(mat)
        sm.handleDraw(ctx)
      ctx.restore()
    }
    if (em && this.getEndPoint) {
      var pa = this.getEndPoint()
      if (em.orient != null && em.orient != 'auto')
        pa.angle = em.orient
      var scale = (em.markerUnits == 'strokeWidth') ? ctx.lineWidth : 1
      ctx.save()
        ctx.translate(pa.point[0], pa.point[1])
        ctx.scale(scale, scale)
        ctx.rotate(pa.angle)
        var mat = CanvasSupport.tRotate(
          CanvasSupport.tScale(
          CanvasSupport.tTranslate(
            this.currentMatrix.slice(0),
            pa.point[0], pa.point[1]
          ), scale, scale), pa.angle)
        em.__copyMatrix(mat)
        em.handleDraw(ctx)
      ctx.restore()
    }
    if (mm && this.getMidPoints) {
      var pas = this.getMidPoints()
      var scale = (mm.markerUnits == 'strokeWidth') ? ctx.lineWidth : 1
      for (var i=0; i<pas.length; i++) {
        var pa = pas[i]
        ctx.save()
          ctx.translate(pa.point[0], pa.point[1])
          ctx.scale(scale, scale)
          if (mm.orient != null && mm.orient != 'auto')
            pa.angle = em.orient
          ctx.rotate(pa.angle)
          var mat = CanvasSupport.tRotate(
            CanvasSupport.tScale(
            CanvasSupport.tTranslate(
              this.currentMatrix.slice(0),
              pa.point[0], pa.point[1]
            ), scale, scale), pa.angle)
          mm.__copyMatrix(mat)
          mm.handleDraw(ctx)
        ctx.restore()
      }
    }
  },

  getStartPoint : false,
  getEndPoint : false,
  getMidPoints : false,
  getBoundingBox : false

})


/**
  A Line is a line drawn from x1,y1 to x2,y2. Lines are stroked by default.

  @param x1 X-coordinate of the line's first point.
  @param y1 Y-coordinate of the line's first point.
  @param x2 X-coordinate of the line's second point.
  @param y2 Y-coordinate of the line's second point.
  @param config Optional config hash.
  */
Line = Klass(Drawable, {
  x1 : 0,
  y1 : 0,
  x2 : 0,
  y2 : 0,
  stroke : true,

  initialize : function(x1,y1, x2,y2, config) {
    this.x1 = x1
    this.y1 = y1
    this.x2 = x2
    this.y2 = y2
    Drawable.initialize.call(this, config)
  },

  drawGeometry : function(ctx) {
    ctx.moveTo(this.x1, this.y1)
    ctx.lineTo(this.x2, this.y2)
  },

  getStartPoint : function() {
    return {
      point: [this.x1, this.y1],
      angle: Math.atan2(this.y2-this.y1, this.x2-this.x1)
    }
  },

  getEndPoint : function() {
    return {
      point: [this.x2, this.y2],
      angle: Math.atan2(this.y2-this.y1, this.x2-this.x1)
    }
  },

  getBoundingBox : function() {
    return [this.x1, this.y1, this.x2-this.x1, this.y2-this.y1]
  },

  getLength : function() {
    return Curves.lineLength([this.x1, this.y1], [this.x2, this.y2])
  }

})



/**
  Circle is used for creating circular paths.

  Uses context.arc(...).

  Attributes:
    cx, cy, radius, startAngle, endAngle, clockwise, closePath, includeCenter

  @param radius Radius of the circle.
  @param config Optional config hash.
  */
Circle = Klass(Drawable, {
  cx : 0,
  cy : 0,
  radius : 10,
  startAngle : 0,
  endAngle : Math.PI * 2,
  clockwise : false,
  closePath : true,
  includeCenter : false,

  initialize : function(radius, config) {
    if (radius != null) this.radius = radius
    Drawable.initialize.call(this, config)
  },

  /**
    Creates a circular path using ctx.arc(...).

    @param ctx Canvas drawing context.
    */
  drawGeometry : function(ctx) {
    if (this.radius == 0) return
    if (this.includeCenter)
      ctx.moveTo(this.cx, this.cy)
    ctx.arc(this.cx, this.cy, this.radius, this.startAngle, this.endAngle, this.clockwise)
    if (this.closePath) {
      // firefox 2 is buggy without the endpoint
      var x2 = Math.cos(this.endAngle)
      var y2 = Math.sin(this.endAngle)
      ctx.moveTo(this.cx + x2*this.radius, this.cy + y2 * this.radius)
      ctx.closePath()
    }
  },

  /**
    Returns true if the point x,y is inside the radius of the circle.

    The x,y point is in user-space coordinates, meaning that e.g. the point
    5,0 will always be inside a circle with radius of 10 and center at origin,
    regardless of the transform on the circle.

    @param x X-coordinate of the point.
    @param y Y-coordinate of the point.
    @return Whether the point is inside the radius of this circle.
    @type boolean
    */
  isPointInPath : function(x,y) {
    x -= this.cx
    y -= this.cy
    return (x*x + y*y) <= (this.radius*this.radius)
  },

  getBoundingBox : function() {
    return [this.cx-this.radius, this.cy-this.radius,
            2*this.radius, 2*this.radius]
  }
})


/**
  Ellipse is a scaled circle. Except it isn't. Because that wouldn't work in
  Opera.
  */
Ellipse = Klass(Circle, {
  radiusX : 0,
  radiusY : 0,

  initialize : function(radiusX, radiusY, config) {
    this.radiusX = radiusX
    this.radiusY = radiusY
    Circle.initialize.call(this, 1, config)
  },

  drawGeometry : function(ctx) {
    if (this.radiusX == 0 || this.radiusY == 0) return
    var k = 0.5522847498
    var x = this.cx
    var y = this.cy
    var krx = k*this.radiusX
    var kry = k*this.radiusY
    ctx.moveTo(x+this.radiusX, y)
    ctx.bezierCurveTo(x+this.radiusX, y-kry, x+krx, y-this.radiusY, x, y-this.radiusY)
    ctx.bezierCurveTo(x-krx, y-this.radiusY, x-this.radiusX, y-kry, x-this.radiusX, y)
    ctx.bezierCurveTo(x-this.radiusX, y+kry, x-krx, y+this.radiusY, x, y+this.radiusY)
    ctx.bezierCurveTo(x+krx, y+this.radiusY, x+this.radiusX, y+kry, x+this.radiusX, y)
  },

  isPointInPath : function(x, y) {
    // does this work?
    x -= this.cx
    y -= this.cy
    x /= this.radiusX
    y /= this.radiusY
    return (x*x + y*y) <= 1
  },

  getBoundingBox : function() {
    return [this.cx-this.radiusX, this.cy-this.radiusY,
            this.radiusX*2, this.radiusY*2]
  }
})


/**
  A Spiral is a function graph drawn in polar coordinates from startAngle to
  endAngle. And the source of all life energy, etc.
  */
Spiral = Klass(Drawable, {
  cx : 0,
  cy : 0,
  startRadius : 0,
  startAngle : 0,
  endAngle : 0,

  radiusFunction : function(a) {
    return a
  },

  initialize : function(endAngle, config) {
    this.endAngle = endAngle
    Drawable.initialize.call(this, config)
  },

  drawGeometry : function(ctx) {
    var x = this.cx
    var y = this.cy
    var a = this.startAngle
    var r = this.startRadius + this.radiusFunction(a)
    ctx.moveTo(x+Math.cos(a)*r, y-Math.sin(a)*r)
    if (this.startAngle < this.endAngle) {
      a += 0.1
      r = this.startRadius + this.radiusFunction(a)
      while (a < this.endAngle) {
        ctx.lineTo(x+Math.cos(a)*r, y-Math.sin(a)*r)
        a += 0.1
        r = this.startRadius + this.radiusFunction(a)
      }
    } else {
      a -= 0.1
      r = this.startRadius + this.radiusFunction(a)
      while (a > this.endAngle) {
        ctx.lineTo(x+Math.cos(a)*r, y-Math.sin(a)*r)
        a -= 0.1
        r = this.startRadius + this.radiusFunction(a)
      }
    }
    a = this.endAngle
    r = this.startRadius + this.radiusFunction(a)
    ctx.lineTo(x+Math.cos(a)*r, y-Math.sin(a)*r)
  },

  isPointInPath : function(x, y) {
    return false
  }
})


/**
  Rectangle is used for creating rectangular paths.

  Uses context.rect(...).

  Attributes:
    cx, cy, width, height, centered, rx, ry

  If centered is set to true, centers the rectangle on the origin.
  Otherwise the top-left corner of the rectangle is on the origin.

  @param width Width of the rectangle.
  @param height Height of the rectangle.
  @param config Optional config hash.
  */
Rectangle = Klass(Drawable, {
  cx : 0,
  cy : 0,
  x2 : 0,
  y2 : 0,
  width : 0,
  height : 0,
  rx : 0,
  ry : 0,
  centered : false,

  initialize : function(width, height, config) {
    if (width != null) {
      this.width = width
      this.height = width
    }
    if (height != null) this.height = height
    Drawable.initialize.call(this, config)
  },

  /**
    Creates a rectangular path using ctx.rect(...).

    @param ctx Canvas drawing context.
    */
  drawGeometry : function(ctx) {
    var x = this.cx
    var y = this.cy
    var w = (this.width || (this.x2 - x))
    var h = (this.height || (this.y2 - y))
    if (w == 0 || h == 0) return
    if (this.centered) {
      x -= 0.5*w
      y -= 0.5*h
    }
    if (this.rx || this.ry) {
      // hahaa, welcome to the undocumented rounded corners path
      // using bezier curves approximating ellipse quadrants
      var rx = Math.min(w * 0.5, this.rx || this.ry)
      var ry = Math.min(h * 0.5, this.ry || rx)
      var k = 0.5522847498
      var krx = k*rx
      var kry = k*ry
      ctx.moveTo(x+rx, y)
      ctx.lineTo(x-rx+w, y)
      ctx.bezierCurveTo(x-rx+w + krx, y, x+w, y+ry-kry, x+w, y+ry)
      ctx.lineTo(x+w, y+h-ry)
      ctx.bezierCurveTo(x+w, y+h-ry+kry, x-rx+w+krx, y+h, x-rx+w, y+h)
      ctx.lineTo(x+rx, y+h)
      ctx.bezierCurveTo(x+rx-krx, y+h, x, y+h-ry+kry, x, y+h-ry)
      ctx.lineTo(x, y+ry)
      ctx.bezierCurveTo(x, y+ry-kry, x+rx-krx, y, x+rx, y)
      ctx.closePath()
    } else {
      if (w < 0) x += w
      if (h < 0) y += h
      ctx.rect(x, y, Math.abs(w), Math.abs(h))
    }
  },

  /**
    Returns true if the point x,y is inside this rectangle.

    The x,y point is in user-space coordinates, meaning that e.g. the point
    5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
    transform on the rectangle.

    @param x X-coordinate of the point.
    @param y Y-coordinate of the point.
    @return Whether the point is inside this rectangle.
    @type boolean
    */
  isPointInPath : function(x,y) {
    x -= this.cx
    y -= this.cy
    if (this.centered) {
      x += this.width/2
      y += this.height/2
    }
    return (x >= 0 && x <= this.width && y >= 0 && y <= this.height)
  },

  getBoundingBox : function() {
    var x = this.cx
    var y = this.cy
    if (this.centered) {
      x -= this.width/2
      y -= this.height/2
    }
    return [x,y,this.width,this.height]
  }
})


/**
  Polygon is used for creating paths consisting of straight line
  segments.

  Attributes:
    segments - The vertices of the polygon, e.g. [0,0, 1,1, 1,2, 0,1]
    closePath - Whether to close the path, default is true.

  @param segments The vertices of the polygon.
  @param closePath Whether to close the path.
  @param config Optional config hash.
  */
Polygon = Klass(Drawable, {
  segments : [],
  closePath : true,

  initialize : function(segments, config) {
    this.segments = segments
    Drawable.initialize.call(this, config)
  },

  drawGeometry : function(ctx) {
    if (!this.segments || this.segments.length < 2) return
    var s = this.segments
    ctx.moveTo(s[0], s[1])
    for (var i=2; i<s.length; i+=2) {
      ctx.lineTo(s[i], s[i+1])
    }
    if (this.closePath)
      ctx.closePath()
  },

  isPointInPath : function(px,py) {
    if (!this.segments || this.segments.length < 2) return false
    var bbox = this.getBoundingBox()
    return (px >= bbox[0] && px <= bbox[0]+bbox[2] &&
            py >= bbox[1] && py <= bbox[1]+bbox[3])
  },

  getStartPoint : function() {
    if (!this.segments || this.segments.length < 2)
      return {point:[0,0], angle:0}
    var a = 0
    if (this.segments.length > 2) {
      a = Curves.lineAngle(this.segments.slice(0,2), this.segments.slice(2,4))
    }
    return {point: this.segments.slice(0,2),
            angle: a}
  },

  getEndPoint : function() {
    if (!this.segments || this.segments.length < 2)
      return {point:[0,0], angle:0}
    var a = 0
    if (this.segments.length > 2) {
      a = Curves.lineAngle(this.segments.slice(-4,-2), this.segments.slice(-2))
    }
    return {point: this.segments.slice(-2),
            angle: a}
  },

  getMidPoints : function() {
    if (!this.segments || this.segments.length < 2)
      return []
    var segs = this.segments
    var verts = []
    for (var i=2; i<segs.length-2; i+=2) {
      var a = segs.slice(i-2,i)
      var b = segs.slice(i, i+2)
      var c = segs.slice(i+2, i+4)
      var t = 0.5 * (Curves.lineAngle(a,b) + Curves.lineAngle(b,c))
      verts.push(
        {point: b, angle: t}
      )
    }
    return verts
  },

  getBoundingBox : function() {
    var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
    var s = this.segments
    for (var i=0; i<s.length; i+=2) {
      var x = s[i], y = s[i+1]
      if (x < minX) minX = x
      if (x > maxX) maxX = x
      if (y < minY) minY = y
      if (y > maxY) maxY = y
    }
    return [minX, minY, maxX-minX, maxY-minY]
  }
})



/**
  CatmullRomSpline draws a Catmull-Rom spline, with optional looping and
  path closing. Handy for motion paths.

  @param segments Control points for the spline, as [[x,y], [x,y], ...]
  @param config Optional config hash.
  */
CatmullRomSpline = Klass(Drawable, {
  segments : [],
  loop : false,
  closePath : false,

  initialize : function(segments, config) {
    this.segments = segments
    Drawable.initialize.call(this, config)
  },

  drawGeometry : function(ctx) {
    var x1 = this.currentMatrix[0]
    var x2 = this.currentMatrix[1]
    var y1 = this.currentMatrix[2]
    var y2 = this.currentMatrix[3]
    var xs = x1*x1 + x2*x2
    var ys = y1*y1 + y2*y2
    var s = Math.floor(Math.sqrt(Math.max(xs, ys)))
    var cmp = this.compiled
    if (!cmp || cmp.scale != s) {
      cmp = this.compile(s)
    }
    for (var i=0; i<cmp.length; i++) {
      var cmd = cmp[i]
      ctx[cmd[0]].apply(ctx, cmd[1])
    }
    if (this.closePath)
      ctx.closePath()
  },

  compile : function(scale) {
    if (!scale) scale = 1
    var compiled = []
    if (this.segments && this.segments.length >= (this.loop ? 1 : 4)) {
      var segs = this.segments
      if (this.loop) {
        segs = segs.slice(0)
        segs.unshift(segs[segs.length-1])
        segs.push(segs[1])
        segs.push(segs[2])
      }
      // FIXME don't be stupid
      var point_spacing = 1 / (15 * (scale+0.5))
      var a,b,c,d,p,pp
      compiled.push(['moveTo', segs[1].slice(0)])
      p = segs[1]
      for (var j=1; j<segs.length-2; j++) {
        a = segs[j-1]
        b = segs[j]
        c = segs[j+1]
        d = segs[j+2]
        for (var i=0; i<1; i+=point_spacing) {
          pp = p
          p = Curves.catmullRomPoint(a,b,c,d,i)
          compiled.push(['lineTo', p])
        }
      }
      p = Curves.catmullRomPoint(a,b,c,d,1)
      compiled.push(['lineTo', p])
    }
    compiled.scale = scale
    this.compiled = compiled
    return compiled
  },

  getBoundingBox : function() {
    var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
    var segments = (this.compiled ? this.compiled : this.compile())
    for (var i=0; i<segments.length; i++) {
      var seg = segments[i][1]
      for (var j=0; j<seg.length; j+=2) {
        var x = seg[j], y = seg[j+1]
        if (x < minX) minX = x
        if (x > maxX) maxX = x
        if (y < minY) minY = y
        if (y > maxY) maxY = y
      }
    }
    return [minX, minY, maxX-minX, maxY-minY]
  },

  pointAt : function(t) {
    if (!this.segments) return [0,0]
    if (this.segments.length >= (this.loop ? 1 : 4)) {
      var segs = this.segments
      if (this.loop) {
        segs = segs.slice(0)
        segs.unshift(segs[segs.length-1])
        segs.push(segs[1])
        segs.push(segs[2])
      }
      // turn t into segment_index.segment_t
      var rt = t * (segs.length - 3)
      var j = Math.floor(rt)
      var st = rt-j
      var a = segs[j],
          b = segs[j+1],
          c = segs[j+2],
          d = segs[j+3]
      return Curves.catmullRomPoint(a,b,c,d,st)
    } else {
      return this.segments[0]
    }
  },

  pointAngleAt : function(t) {
    if (!this.segments) return {point: [0,0], angle: 0}
    if (this.segments.length >= (this.loop ? 1 : 4)) {
      var segs = this.segments
      if (this.loop) {
        segs = segs.slice(0)
        segs.unshift(segs[segs.length-1])
        segs.push(segs[1])
        segs.push(segs[2])
      }
      // turn t into segment_index.segment_t
      var rt = t * (segs.length - 3)
      var j = Math.floor(rt)
      var st = rt-j
      var a = segs[j],
          b = segs[j+1],
          c = segs[j+2],
          d = segs[j+3]
      return Curves.catmullRomPointAngle(a,b,c,d,st)
    } else {
      return {point:this.segments[0] || [0,0], angle: 0}
    }
  }
})


/**
  Path is used for creating custom paths.

  Attributes: segments, closePath.

    var path = new Path([
      ['moveTo', [-50, -60]],
      ['lineTo', [30, 50],
      ['lineTo', [-50, 50]],
      ['bezierCurveTo', [-50, 100, -50, 100, 0, 100]],
      ['quadraticCurveTo', [0, 120, -20, 130]],
      ['quadraticCurveTo', [0, 140, 0, 160]],
      ['bezierCurveTo', [-10, 160, -20, 170, -30, 180]],
      ['quadraticCurveTo', [10, 230, -50, 260]]
    ])

  The path segments are used as [methodName, arguments] on the canvas
  drawing context, so the possible path segments are:

    ['moveTo', [x, y]]
    ['lineTo', [x, y]]
    ['quadraticCurveTo', [control_point_x, control_point_y, x, y]]
    ['bezierCurveTo', [cp1x, cp1y, cp2x, cp2y, x, y]]
    ['arc', [x, y, radius, startAngle, endAngle, drawClockwise]]
    ['arcTo', [x1, y1, x2, y2, radius]]
    ['rect', [x, y, width, height]]

  You can also pass an SVG path string as segments.

    var path = new Path("M 100 100 L 300 100 L 200 300 z", {
      stroke: true, strokeStyle: 'blue',
      fill: true, fillStyle: 'red',
      lineWidth: 3
    })

  @param segments The path segments.
  @param config Optional config hash.
  */
Path = Klass(Drawable, {
  segments : [],
  closePath : false,

  initialize : function(segments, config) {
    this.segments = segments
    Drawable.initialize.call(this, config)
  },

  /**
    Creates a path on the given drawing context.

    For each path segment, calls the context method named in the first element
    of the segment with the rest of the segment elements as arguments.

    SVG paths are parsed and executed.

    Closes the path if closePath is true.

    @param ctx Canvas drawing context.
    */
  drawGeometry : function(ctx) {
    var segments = this.getSegments()
    for (var i=0; i<segments.length; i++) {
      var seg = segments[i]
      ctx[seg[0]].apply(ctx, seg[1])
    }
    if (this.closePath)
      ctx.closePath()
  },

  /**
    Returns true if the point x,y is inside the path's bounding rectangle.

    The x,y point is in user-space coordinates, meaning that e.g. the point
    5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
    transform on the rectangle.

    @param px X-coordinate of the point.
    @param py Y-coordinate of the point.
    @return Whether the point is inside the path's bounding rectangle.
    @type boolean
    */
  isPointInPath : function(px,py) {
    var bbox = this.getBoundingBox()
    return (px >= bbox[0] && px <= bbox[0]+bbox[2] &&
            py >= bbox[1] && py <= bbox[1]+bbox[3])
  },

  getBoundingBox : function() {
    if (!(this.compiled && this.compiledBoundingBox)) {
      var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
      var segments = this.getSegments()
      for (var i=0; i<segments.length; i++) {
        var seg = segments[i][1]
        for (var j=0; j<seg.length; j+=2) {
          var x = seg[j], y = seg[j+1]
          if (x < minX) minX = x
          if (x > maxX) maxX = x
          if (y < minY) minY = y
          if (y > maxY) maxY = y
        }
      }
      this.compiledBoundingBox = [minX, minY, maxX-minX, maxY-minY]
    }
    return this.compiledBoundingBox
  },

  getStartPoint : function() {
    var segs = this.getSegments()
    if (!segs || !segs[0]) return {point: [0,0], angle: 0}
    var fs = segs[0]
    var c = fs[1]
    var point = [c[c.length-2], c[c.length-1]]
    var ss = segs[1]
    var angle = 0
    if (ss) {
      c2 = ss[1]
      angle = Curves.lineAngle(point, [c2[c2.length-2], c2[c2.length-1]])
    }
    return {
      point: point,
      angle: angle
    }
  },

  getEndPoint : function() {
    var segs = this.getSegments()
    if (!segs || !segs[0]) return {point: [0,0], angle: 0}
    var fs = segs[segs.length-1]
    var c = fs[1]
    var point = [c[c.length-2], c[c.length-1]]
    var ss = segs[segs.length-2]
    var angle = 0
    if (ss) {
      c2 = ss[1]
      angle = Curves.lineAngle([c2[c2.length-2], c2[c2.length-1]], point)
    }
    return {
      point: point,
      angle: angle
    }
  },

  getMidPoints : function() {
    var segs = this.getSegments()
    if (this.vertices)
      return this.vertices.slice(1,-1)
    var verts = []
    for (var i=1; i<segs.length-1; i++) {
      var b = segs[i-1][1].slice(-2)
      var c = segs[i][1].slice(0,2)
      if (segs[i-1].length > 2) {
        var a = segs[i-1][1].slice(-4,-2)
        var t = 0.5 * (Curves.lineAngle(a,b) + Curves.lineAngle(b,c))
      } else {
        var t = Curves.lineAngle(b,c)
      }
      verts.push(
        {point: b, angle: t}
      )
      var id = segs[i][2]
      if (id != null) {
        i++
        while (segs[i] && segs[i][2] == id) i++
        i--
      }
    }
    return verts
  },

  getSegments : function() {
    if (typeof(this.segments) == 'string') {
      if (!this.compiled || this.segments != this.compiledSegments) {
        this.compiled = this.compileSVGPath(this.segments)
        this.compiledSegments = this.segments
      }
    } else if (!this.compiled) {
      this.compiled = Object.clone(this.segments)
    }
    return this.compiled
  },

  /**
    Compiles an SVG path string into an array of canvas context method calls.

    Returns an array of [methodName, [arg1, arg2, ...]] method call arrays.
    */
  compileSVGPath : function(svgPath) {
    var segs = svgPath.split(/(?=[a-z])/i)
    var x = 0
    var y = 0
    var px,py
    var pc
    var commands = []
    for (var i=0; i<segs.length; i++) {
      var seg = segs[i]
      var cmd = seg.match(/[a-z]/i)
      if (!cmd) return [];
      cmd = cmd[0];
      var coords = seg.match(/[+-]?\d+(\.\d+(e\d+(\.\d+)?)?)?/gi)
      if (coords) coords = coords.map(parseFloat)
      switch(cmd) {
        case 'M':
          x = coords[0]
          y = coords[1]
          px = py = null
          commands.push(['moveTo', [x, y]])
          break
        case 'm':
          x += coords[0]
          y += coords[1]
          px = py = null
          commands.push(['moveTo', [x, y]])
          break

        case 'L':
          x = coords[0]
          y = coords[1]
          px = py = null
          commands.push(['lineTo', [x, y]])
          break
        case 'l':
          x += coords[0]
          y += coords[1]
          px = py = null
          commands.push(['lineTo', [x, y]])
          break
        case 'H':
          x = coords[0]
          px = py = null
          commands.push(['lineTo', [x, y]])
          break
        case 'h':
          x += coords[0]
          px = py = null
          commands.push(['lineTo', [x,y]])
          break
        case 'V':
          y = coords[0]
          px = py = null
          commands.push(['lineTo', [x,y]])
          break
        case 'v':
          y += coords[0]
          px = py = null
          commands.push(['lineTo', [x,y]])
          break

        case 'C':
          x = coords[4]
          y = coords[5]
          px = coords[2]
          py = coords[3]
          commands.push(['bezierCurveTo', coords])
          break
        case 'c':
          commands.push(['bezierCurveTo',[
            coords[0] + x, coords[1] + y,
            coords[2] + x, coords[3] + y,
            coords[4] + x, coords[5] + y
          ]])
          px = x + coords[2]
          py = y + coords[3]
          x += coords[4]
          y += coords[5]
          break

        case 'S':
          if (px == null || !pc.match(/[sc]/i)) {
            px = x
            py = y
          }
          commands.push(['bezierCurveTo',[
            x-(px-x), y-(py-y),
            coords[0], coords[1],
            coords[2], coords[3]
          ]])
          px = coords[0]
          py = coords[1]
          x = coords[2]
          y = coords[3]
          break
        case 's':
          if (px == null || !pc.match(/[sc]/i)) {
            px = x
            py = y
          }
          commands.push(['bezierCurveTo',[
            x-(px-x), y-(py-y),
            x + coords[0], y + coords[1],
            x + coords[2], y + coords[3]
          ]])
          px = x + coords[0]
          py = y + coords[1]
          x += coords[2]
          y += coords[3]
          break

        case 'Q':
          px = coords[0]
          py = coords[1]
          x = coords[2]
          y = coords[3]
          commands.push(['quadraticCurveTo', coords])
          break
        case 'q':
          commands.push(['quadraticCurveTo',[
            coords[0] + x, coords[1] + y,
            coords[2] + x, coords[3] + y
          ]])
          px = x + coords[0]
          py = y + coords[1]
          x += coords[2]
          y += coords[3]
          break

        case 'T':
          if (px == null || !pc.match(/[qt]/i)) {
            px = x
            py = y
          } else {
            px = x-(px-x)
            py = y-(py-y)
          }
          commands.push(['quadraticCurveTo',[
            px, py,
            coords[0], coords[1]
          ]])
          px = x-(px-x)
          py = y-(py-y)
          x = coords[0]
          y = coords[1]
          break
        case 't':
          if (px == null || !pc.match(/[qt]/i)) {
            px = x
            py = y
          } else {
            px = x-(px-x)
            py = y-(py-y)
          }
          commands.push(['quadraticCurveTo',[
            px, py,
            x + coords[0], y + coords[1]
          ]])
          x += coords[0]
          y += coords[1]
          break

        case 'A':
          var arc_segs = this.solveArc(x,y, coords)
          for (var l=0; l<arc_segs.length; l++) arc_segs[l][2] = i
          commands.push.apply(commands, arc_segs)
          x = coords[5]
          y = coords[6]
          break
        case 'a':
          coords[5] += x
          coords[6] += y
          var arc_segs = this.solveArc(x,y, coords)
          for (var l=0; l<arc_segs.length; l++) arc_segs[l][2] = i
          commands.push.apply(commands, arc_segs)
          x = coords[5]
          y = coords[6]
          break

        case 'Z':
          commands.push(['closePath', []])
          break
        case 'z':
          commands.push(['closePath', []])
          break
      }
      pc = cmd
    }
    return commands
  },

  solveArc : function(x, y, coords) {
    var rx = coords[0]
    var ry = coords[1]
    var rot = coords[2]
    var large = coords[3]
    var sweep = coords[4]
    var ex = coords[5]
    var ey = coords[6]
    var segs = this.arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y)
    var retval = []
    for (var i=0; i<segs.length; i++) {
      retval.push(['bezierCurveTo', this.segmentToBezier.apply(this, segs[i])])
    }
    return retval
  },


  // Copied from Inkscape svgtopdf, thanks!
  arcToSegments : function(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
    var th = rotateX * (Math.PI/180)
    var sin_th = Math.sin(th)
    var cos_th = Math.cos(th)
    rx = Math.abs(rx)
    ry = Math.abs(ry)
    var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5
    var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5
    var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry)
    if (pl > 1) {
      pl = Math.sqrt(pl)
      rx *= pl
      ry *= pl
    }

    var a00 = cos_th / rx
    var a01 = sin_th / rx
    var a10 = (-sin_th) / ry
    var a11 = (cos_th) / ry
    var x0 = a00 * ox + a01 * oy
    var y0 = a10 * ox + a11 * oy
    var x1 = a00 * x + a01 * y
    var y1 = a10 * x + a11 * y

    var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0)
    var sfactor_sq = 1 / d - 0.25
    if (sfactor_sq < 0) sfactor_sq = 0
    var sfactor = Math.sqrt(sfactor_sq)
    if (sweep == large) sfactor = -sfactor
    var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0)
    var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0)

    var th0 = Math.atan2(y0-yc, x0-xc)
    var th1 = Math.atan2(y1-yc, x1-xc)

    var th_arc = th1-th0
    if (th_arc < 0 && sweep == 1){
      th_arc += 2*Math.PI
    } else if (th_arc > 0 && sweep == 0) {
      th_arc -= 2 * Math.PI
    }

    var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001)))
    var result = []
    for (var i=0; i<segments; i++) {
      var th2 = th0 + i * th_arc / segments
      var th3 = th0 + (i+1) * th_arc / segments
      result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th]
    }

    return result
  },

  segmentToBezier : function(cx, cy, th0, th1, rx, ry, sin_th, cos_th) {
    var a00 = cos_th * rx
    var a01 = -sin_th * ry
    var a10 = sin_th * rx
    var a11 = cos_th * ry

    var th_half = 0.5 * (th1 - th0)
    var t = (8/3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half)
    var x1 = cx + Math.cos(th0) - t * Math.sin(th0)
    var y1 = cy + Math.sin(th0) + t * Math.cos(th0)
    var x3 = cx + Math.cos(th1)
    var y3 = cy + Math.sin(th1)
    var x2 = x3 + t * Math.sin(th1)
    var y2 = y3 - t * Math.cos(th1)
    return [
      a00 * x1 + a01 * y1,      a10 * x1 + a11 * y1,
      a00 * x2 + a01 * y2,      a10 * x2 + a11 * y2,
      a00 * x3 + a01 * y3,      a10 * x3 + a11 * y3
    ]
  },

  getLength : function() {
    var segs = this.getSegments()
    if (segs.arcLength == null) {
      segs.arcLength = 0
      var x=0, y=0
      for (var i=0; i<segs.length; i++) {
        var args = segs[i][1]
        if (args.length < 2) continue
        switch(segs[i][0]) {
          case 'bezierCurveTo':
            segs[i][3] = Curves.cubicLength(
              [x, y], [args[0], args[1]], [args[2], args[3]], [args[4], args[5]])
            break
          case 'quadraticCurveTo':
            segs[i][3] = Curves.quadraticLength(
              [x, y], [args[0], args[1]], [args[2], args[3]])
            break
          case 'lineTo':
            segs[i][3] = Curves.lineLength(
              [x, y], [args[0], args[1]])
            break
        }
        if (segs[i][3])
          segs.arcLength += segs[i][3]
        x = args[args.length-2]
        y = args[args.length-1]
      }
    }
    return segs.arcLength
  },

  pointAngleAt : function(t, config) {
    var segments = []
    var segs = this.getSegments()
    var length = this.getLength()
    var x = 0, y = 0
    for (var i=0; i<segs.length; i++) {
      var seg = segs[i]
      if (seg[1].length < 2) continue
      if (seg[0] != 'moveTo') {
        segments.push([x, y, seg])
      }
      x = seg[1][seg[1].length-2]
      y = seg[1][seg[1].length-1]
    }
    if (segments.length < 1)
      return {point: [x, y], angle: 0 }
    if (t >= 1) {
      var rt = 1
      var seg = segments[segments.length-1]
    } else if (config && config.discrete) {
      var idx = Math.floor(t * segments.length)
      var seg = segments[idx]
      var rt = 0
    } else if (config && config.linear) {
      var idx = t * segments.length
      var rt = idx - Math.floor(idx)
      var seg = segments[Math.floor(idx)]
    } else {
      var len = t * length
      var rlen = 0, idx, rt
      for (var i=0; i<segments.length; i++) {
        if (rlen + segments[i][2][3] > len) {
          idx = i
          rt = (len - rlen) / segments[i][2][3]
          break
        }
        rlen += segments[i][2][3]
      }
      var seg = segments[idx]
    }
    var angle = 0
    var cmd = seg[2][0]
    var args = seg[2][1]
    switch (cmd) {
      case 'bezierCurveTo':
        return Curves.cubicLengthPointAngle([seg[0], seg[1]], [args[0], args[1]], [args[2], args[3]], [args[4], args[5]], rt)
        break
      case 'quadraticCurveTo':
        return Curves.quadraticLengthPointAngle([seg[0], seg[1]], [args[0], args[1]], [args[2], args[3]], rt)
        break
      case 'lineTo':
        x = Curves.linearValue(seg[0], args[0], rt)
        y = Curves.linearValue(seg[1], args[1], rt)
        angle = Curves.lineAngle([seg[0], seg[1]], [args[0], args[1]], rt)
        break
    }
    return {point: [x, y], angle: angle }
  }

})


/**
  ImageNode is used for drawing images. Creates a rectangular path around
  the drawn image.

  Attributes:

    centered - If true, image center is at the origin.
               Otherwise image top-left is at the origin.
    usePattern - Use a pattern fill for drawing the image (instead of
                 drawImage.) Doesn't do sub-image drawing, and Safari doesn't
                 like scaled image patterns.
    sX, sY, sWidth, sHeight - Area of image to draw. Optional.
    dX, dY - Coordinates where to draw the image. Default is 0, 0.
    dWidth, dHeight - Size of the drawn image. Optional.

  Example:

    var img = new Image()
    img.src = 'foo.jpg'
    var imageGeo = new ImageNode(img)

  @param image Image to draw.
  @param config Optional config hash.
  */
ImageNode = Klass(Drawable, {
  centered : false,
  usePattern : false,

  sX : 0,
  sY : 0,
  sWidth : null,
  sHeight : null,

  dX : 0,
  dY : 0,
  dWidth : null,
  dHeight : null,

  initialize : function(image, config) {
    this.image = image
    Drawable.initialize.call(this, config)
  },

  /**
    Draws the image on the given drawing context.

    Creates a rectangular path around the drawn image (for possible stroke
    and/or fill.)

    @param ctx Canvas drawing context.
    */
  drawGeometry : function(ctx) {
    if (Object.isImageLoaded(this.image)) {
      var w = this.dWidth == null ? this.image.width : this.dWidth
      var h = this.dHeight == null ? this.image.height : this.dHeight
      var x = this.dX + (this.centered ? -w * 0.5 : 0)
      var y = this.dY + (this.centered ? -h * 0.5 : 0)
      if (this.dWidth != null) {
        if (this.sWidth != null) {
          ctx.drawImage(this.image,
            this.sX, this.sY, this.sWidth, this.sHeight,
            x, y, w, h)
        } else {
          ctx.drawImage(this.image, x, y, w, h)
        }
      } else {
        w = this.image.width
        h = this.image.height
        if (this.usePattern) {
          if (!this.imagePattern)
            this.imagePattern = new Pattern(this.image, 'repeat')
          var fs = this.imagePattern.compiled
          if (!fs)
            fs = this.imagePattern.compile(ctx)
          ctx.save()
          ctx.beginPath()
          ctx.rect(x, y, w, h)
          ctx.setFillStyle(fs)
          ctx.fill()
          ctx.restore()
          ctx.beginPath()
        } else {
          ctx.drawImage(this.image, x, y)
        }
      }
    } else {
      var w = this.dWidth
      var h = this.dHeight
      if (!( w && h )) return
      var x = this.dX + (this.centered ? -w * 0.5 : 0)
      var y = this.dY + (this.centered ? -h * 0.5 : 0)
    }
    ctx.rect(x, y, w, h)
  },

  /**
    Creates a bounding rectangle path for the image on the given drawing
    context.

    @param ctx Canvas drawing context.
    */
  drawPickingPath : function(ctx) {
    var x = this.dX + (this.centered ? -this.image.width * 0.5 : 0)
    var y = this.dY + (this.centered ? -this.image.height * 0.5 : 0)
    var w = this.dWidth
    var h = this.dHeight
    if (this.dWidth == null) {
      w = this.image.width
      h = this.image.height
    }
    ctx.rect(x, y, w, h)
  },

  /**
    Returns true if the point x,y is inside the image rectangle.

    The x,y point is in user-space coordinates, meaning that e.g. the point
    5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
    transform on the rectangle.

    @param x X-coordinate of the point.
    @param y Y-coordinate of the point.
    @return Whether the point is inside the image rectangle.
    @type boolean
    */
  isPointInPath : function(x,y) {
    x -= this.dX
    y -= this.dY
    if (this.centered) {
      x += this.image.width * 0.5
      y += this.image.height * 0.5
    }
    var w = this.dWidth
    var h = this.dHeight
    if (this.dWidth == null) {
      w = this.image.width
      h = this.image.height
    }
    return ((x >= 0) && (x <= w) && (y >= 0) && (y <= h))
  },

  getBoundingBox : function() {
    x = this.dX
    y = this.dY
    if (this.centered) {
      x -= this.image.width * 0.5
      y -= this.image.height * 0.5
    }
    var w = this.dWidth
    var h = this.dHeight
    if (this.dWidth == null) {
      w = this.image.width
      h = this.image.height
    }
    return [x, y, w, h]
  }
})

ImageNode.load = function(src) {
  var img = new Image();
  img.src = src;
  var imgn = new ImageNode(img);
  return imgn;
}


/**
  TextNode is used for drawing text on a canvas.

  Attributes:

    text - The text string to draw.
    align - Horizontal alignment for the text.
            'left', 'right', 'center', 'start' or 'end'
    baseline - Baseline used for the text.
               'top', 'hanging', 'middle', 'alphabetic', 'ideographic' or 'bottom'
    asPath - If true, creates a text path instead of drawing the text.
    pathGeometry - A geometry object the path of which the text follows.

  Example:

    var text = new TextGeometry('The cake is a lie.')

  @param text The text string to draw.
  @param config Optional config hash.
  */
TextNode = Klass(Drawable, {
  text : 'Text',
  align : 'start', // 'left' | 'right' | 'center' | 'start' | 'end'
  baseline : 'alphabetic', // 'top' | 'hanging' | 'middle' | 'alphabetic' |
                           // 'ideographic' | 'bottom'
  accuratePicking : false,
  asPath : false,
  pathGeometry : null,
  maxWidth : null,
  width : 0,
  height : 20,
  cx : 0,
  cy : 0,

  __drawMethodName : 'draw' + CanvasSupport.getTextBackend(),
  __pickingMethodName : 'drawPickingPath' + CanvasSupport.getTextBackend(),

  initialize : function(text, config) {
    this.lastText = this.text
    this.text = text
    Drawable.initialize.call(this, config)
  },

  drawGeometry : function(ctx) {
    this.drawUsing(ctx, this.__drawMethodName)
  },

  drawPickingPath : function(ctx) {
    this.drawUsing(ctx, this.__pickingMethodName)
  },

  drawUsing : function(ctx, methodName) {
    if (!this.text || this.text.length == 0)
      return
    if (this.lastText != this.text || this.lastStyle != ctx.font) {
      this.dimensions = this.measureText(ctx)
      this.lastText = this.text
      this.lastStyle = ctx.font
    }
    if (this[methodName])
      this[methodName](ctx)
  },

  measureText : function(ctx) {
    var mn = 'measureText' + CanvasSupport.getTextBackend().capitalize()
    if (this[mn]) {
      return this[mn](ctx)
    } else {
      return {width: 0, height: 0}
    }
  },

  computeXForAlign : function() {
    if (this.align == 'left') // most hit branch
      return 0
    else if (this.align == 'right')
      return -this.dimensions.width
    else if (this.align == 'center')
      return  -this.dimensions.width * 0.5
  },

  measureTextHTML5 : function(ctx) {
    // FIXME measureText is retarded
    return {width: ctx.measureText(this.text).width, height: 20}
  },

  drawHTML5 : function(ctx) {
    ctx.fillText(this.text, this.cx, this.cy, this.maxWidth)
  },

  drawPickingPathHTML5 : function(ctx) {
    var ascender = 15 // this.dimensions.ascender
    var ry = this.cy - ascender
    ctx.rect(this.cx, ry, this.dimensions.width, this.dimensions.height)
  },

  measureTextMozText : function(ctx) {
    return {width: ctx.mozMeasureText(this.text), height: 20}
  },

  drawMozText : function(ctx) {
    var x = this.cx + this.computeXForAlign()
    var y = this.cy + 0
    if (this.pathGeometry) {
      this.pathGeometry.draw(ctx)
      ctx.mozDrawTextAlongPath(this.text, this.path)
    } else {
      ctx.save()
      ctx.translate(x,y)
      if (this.asPath) {
        ctx.mozPathText(this.text)
      } else {
        ctx.mozDrawText(this.text)
      }
      ctx.restore()
    }
  },

  drawPickingPathMozText : function(ctx) {
    var x = this.cx + this.computeXForAlign()
    var y = this.cy + 0
    if (this.pathGeometry) { // FIXME how to draw a text path along path?
        this.pathGeometry.draw(ctx)
        // ctx.mozDrawTextAlongPath(this.text, this.path)
    } else if (!this.accuratePicking) {
      var ascender = 15 // this.dimensions.ascender
      var ry = y - ascender
      ctx.rect(x, ry, this.dimensions.width, this.dimensions.height)
    } else {
      ctx.save()
      ctx.translate(x,y)
      ctx.mozPathText(this.text)
      ctx.restore()
    }
  },

  drawDrawString : function(ctx) {
    var x = this.cx + this.computeXForAlign()
    var y = this.cy + 0
    ctx.drawString(x,y, this.text)
  },

  measureTextPerfectWorld : function(ctx) {
    return ctx.measureText(this.text)
  },

  drawPerfectWorld : function(ctx) {
    if (this.pathGeometry) {
      this.pathGeometry.draw(ctx)
      if (this.asPath)
        ctx.pathTextAlongPath(this.text)
      else
        ctx.drawTextAlongPath(this.text)
    } else if (this.asPath) {
      ctx.pathText(this.text)
    } else {
      ctx.drawText(this.text)
    }
  },

  drawPickingPathPerfectWorld : function(ctx) {
    if (this.accuratePicking) {
      if (this.pathGeometry) {
        ctx.pathTextAlongPath(this.text)
      } else {
        ctx.pathText(this.text)
      }
    } else { // creates a path of text bounding box
      if (this.pathGeometry) {
        ctx.textRectAlongPath(this.text)
      } else {
        ctx.textRect(this.text)
      }
    }
  }
})


/**
  Gradient is a linear or radial color gradient that can be used as a
  strokeStyle or fillStyle.

  Attributes:

    type - Type of the gradient. 'linear' or 'radial'
    startX, startY - Coordinates for the starting point of the gradient.
                     Center of the starting circle of a radial gradient.
                     Default is 0, 0.
    endX, endY - Coordinates for the ending point of the gradient.
                 Center of the ending circle of a radial gradient.
                 Default is 0, 0.
    startRadius - The radius of the starting circle of a radial gradient.
                  Default is 0.
    endRadius - The radius of the ending circle of a radial gradient.
                Default is 100.
    colorStops - The color stops for the gradient. The format for the color
                 stops is: [[position_1, color_1], [position_2, color_2], ...].
                 The possible color formats are: 'red', '#000', '#000000',
                 'rgba(0,0,0, 0.2)', [0,0,0] and [0,0,0, 0.2].
                 Default color stops are [[0, '#000000'], [1, '#FFFFFF']].

  Example:

    var g = new Gradient({
      type : 'radial',
      endRadius : 40,
      colorStops : [
        [0, '#000'],
        [0.2, '#ffffff'],
        [0.5, [255, 0, 0]],
        [0.8, [0, 255, 255, 0.5]],
        [1.0, 'rgba(255, 0, 255, 0.8)']
      ]
    })

  @param config Optional config hash.
  */
Gradient = Klass({
  type : 'linear',
  isPattern : true,
  startX : 0,
  startY : 0,
  endX : 1,
  endY : 0,
  startRadius : 0,
  endRadius : 1,
  colorStops : [],

  initialize : function(config) {
    this.colorStops = [[0, '#000000'], [1, '#FFFFFF']]
    if (config) Object.extend(this, config)
  },

  /**
    Compiles the gradient using the given drawing context.
    Returns a gradient object that can be used as drawing context
    fill/strokeStyle.

    @param ctx Drawing context to compile pattern on.
    @return Gradient object.
    */
  compile : function(ctx) {
    if (this.type == 'linear') {
      var go = ctx.createLinearGradient(
                    this.startX, this.startY,
                    this.endX, this.endY)
    } else {
      var go = ctx.createRadialGradient(
                    this.startX, this.startY, this.startRadius,
                    this.endX, this.endY, this.endRadius)
    }
    for(var i=0; i<this.colorStops.length; i++) {
      var cs = this.colorStops[i]
      if (typeof(cs[1]) == 'string') {
        go.addColorStop(cs[0], cs[1])
      } else {
        var ca = cs[1]
        var a = (ca.length == 3) ? 1 : ca[3]
        var g = 'rgba('+ca.slice(0,3).map(Math.round).join(",")+', '+a+')'
        go.addColorStop(cs[0], g)
      }
    }
    Object.extend(go, Transformable.prototype)
    go.transformList = this.transformList
    go.scale = this.scale
    go.x = this.x
    go.y = this.y
    go.matrix = this.matrix
    go.rotation = this.rotation
    go.units = this.units
    if (!go.isMockObject)
      this.compiled = go
    return go
  }
})


/**
  Pattern is a possibly repeating image that can be used as a strokeStyle or
  fillStyle.

    var image = new Image()
    image.src = 'foo.jpg'
    var pattern = new Pattern(image, 'no-repeat')
    var rect = new Rectangle(200, 200, {fill: true, fillStyle: pattern})

  @param image The image object for the pattern. IMG and CANVAS elements, and
               Image objects all work.
  @param repeat The repeat mode of the pattern. One of 'repeat', 'repeat-y',
                'repeat-x' and 'no-repeat'. The default is 'repeat'.
  */
Pattern = Klass({
  isPattern : true,
  repeat: 'repeat',

  initialize : function(image, repeat) {
    this.image = image
    if (repeat)
      this.repeat = repeat
  },

  /**
    Compiles the pattern using the given drawing context.
    Returns a pattern object that can be used as drawing context
    fill/strokeStyle.

    @param ctx Drawing context to compile pattern on.
    @return Pattern object.
    */
  compile : function(ctx) {
    var pat = ctx.createPattern(this.image, this.repeat)
    Object.extend(pat, Transformable.prototype)
    pat.transformList = this.transformList
    pat.scale = this.scale
    pat.x = this.x
    pat.y = this.y
    pat.matrix = this.matrix
    pat.rotation = this.rotation
    pat.units = this.units
    if (!pat.isMockObject)
      this.compiled = pat
    return pat
  }
})












/**
  SVG parser for simple documents. Converts SVG DOM to CAKE scenegraph.
  Emphasis on graphical images, not on the "HTML-killer" features of SVG.

  var svgNode = SVGParser.parse(
    svgRootElement, filename, containerWidth, containerHeight, fontSize
  )

  Features:
    * <svg> width and height, viewBox clipping.
    * Clipping (objectBoundingBox clipping too)
    * Paths, rectangles, ellipses, circles, lines, polylines and polygons
    * Simple untransformed text using HTML
    * Nested transforms
    * Transform lists (transform="rotate(30) translate(2,2) scale(4)")
    * Gradient and pattern transforms
    * Strokes with miter, joins and caps
    * Flat fills and gradient fills, ditto for strokes
    * Parsing simple stylesheets (tag, class or id)
    * Images
    * Non-pixel units (cm, mm, in, pt, pc, em, ex, %)
    * <use>-tags
    * preserveAspectRatio
    * Dynamic gradient sizes (objectBoundingBox, etc.)
    * Markers (though buggy)

  Some of the several missing features:
    * Masks
    * Patterns
    * viewBox clipping for elements other than <marker> and <svg>
    * Text styling
    * tspan, tref, textPath, many things text
    * Fancy style rules (tag .class + #foo > bob#alice { ... })
    * Filters
    * Animation
    * Dashed strokes
  */
SVGParser = {
  /**
    Loads an SVG document using XMLHttpRequest and calls the given onSuccess
    callback with the parsed document. If loading fails, calls onFailure
    instead if one is given. When loading and an onLoading callback is given,
    calls onLoading every time xhr.readyState is 3.

    The callbacks will be called with the following parameters:

    onSuccess(svgNode, xmlHttpRequest,
      filename, config)

    onFailure(xmlHttpRequest, possibleException,
      filename, config)

    onLoading(xmlHttpRequest,
      filename, config)

    Config hash parameters:
      filename: Filename for the SVG document. Used for parsing image paths.
      width: Width of the bounding box to fit the SVG in.
      height: Height of the bounding box to fit the SVG in.
      fontSize: Default font size for the SVG document.
      onSuccess: Function to call on successful load. Required.
      onFailure: Function to call on failed load.
      onLoading: Function to call while loading.

    @param config The config hash.
    @param filename The URL of the SVG document to load. Must conform to SOP.
    */
  load : function(filename, config) {
    if (!config.onSuccess) throw("Need to provide an onSuccess function.")
    if (!config.filename)
      config.filename = filename
    var xhr = window.XMLHttpRequest ? new XMLHttpRequest() :
                                      new ActiveXObject("MSXML2.XMLHTTP.3.0")
    xhr.open('GET', filename, true)
    xhr.overrideMimeType('text/xml')
    var failureFired = false
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 4) {
        if (xhr.status == 200 || xhr.status == 0) {
          try {
            var svg = xhr.responseXML
            var svgNode = SVGParser.parse(svg, config)
            svgNode.svgRootElement = svg
          } catch(e) {
            if (config.onFailure)
              config.onFailure(xhr, e, filename, config)
            return
          }
          config.onSuccess(svgNode, xhr, filename, config)
        } else {
          if (config.onFailure && !failureFired)
            config.onFailure(xhr, null, filename, config)
        }
      } else if (xhr.readyState == 3) {
        if (config.onLoading)
          config.onLoading(xhr, filename, config)
      }
    }
    try {
      xhr.send(null)
    } catch(e) {
      if (config.onFailure) {
        config.onFailure(xhr, e, filename, config)
        failureFired = true
      }
      xhr.abort()
    }
  },

  /**
    Parses an SVG DOM into CAKE scenegraph.

    Config hash parameters:
      filename: Filename for the SVG document. Used for parsing image paths.
      width: Width of the bounding box to fit the SVG in.
      height: Height of the bounding box to fit the SVG in.
      fontSize: Default font size for the SVG document.
      currentColor: HTML text color of the containing element.

    @param svgRootElement The root element of the SVG DOM.
    @param config The config hash.
    @returns The root CanvasNode of the scenegraph created from the SVG document.
    @type CanvasNode
    */
  parse : function(svgRootElement, config) {
    var n = new CanvasNode()
    var w = config.width, h = config.height, fs = config.fontSize
    n.innerWidth = w || window.innerWidth
    n.innerHeight = h || window.innerHeight
    n.innerSize = Math.sqrt(w*w + h*h) / Math.sqrt(2)
    n.fontSize = fs || 12
    n.filename = config.filename || '.'
    var defs = {}
    var style = { ids : {}, classes : {}, tags : {} }
    n.color = config.currentColor || 'black'
    this.parseChildren(svgRootElement, n, defs, style)
    n.defs = defs
    n.style = style
    return n
  },

  parsePreserveAspectRatio : function(aspect, w, h, vpw, vph) {
    var aspect = aspect || ""
    var aspa = aspect.split(/\s+/)
    var defer = (aspa[0] == 'defer')
    if (defer) aspa.shift()
    var align = (aspa[0] || 'xMidYMid')
    var meet = (aspa[1] || 'meet')
    var wf = w / vpw
    var hf = h / vph
    var xywh = {x:0, y:0, w:wf, h:hf}
    if (align == 'none') return xywh
    xywh.w = xywh.h = (meet == 'meet' ? Math.min : Math.max)(wf, hf)
    var xa = align.slice(1, 4).toLowerCase()
    var ya = align.slice(5, 8).toLowerCase()
    var xf = (this.SVGAlignMap[xa] || 0)
    var yf = (this.SVGAlignMap[ya] || 0)
    xywh.x = xf * (w-vpw*xywh.w)
    xywh.y = yf * (h-vph*xywh.h)
    return xywh
  },

  SVGAlignMap : {
    min : 0,
    mid : 0.5,
    max : 1
  },

  SVGTagMapping : {
    svg : function(c, cn, defs, style) {
      var p = new Rectangle()
      p.width = 0
      p.height = 0
      p.doFill = function(){}
      p.doStroke = function(){}
      p.drawMarkers = function(){}
      p.fill = 'black'
      p.stroke = 'none'
      var vb = c.getAttribute('viewBox')
      var w = c.getAttribute('width')
      var h = c.getAttribute('height')
      if (!w) w = h
      else if (!h) h = w
      if (w) {
        var wpx = this.parseUnit(w, cn, 'x')
        var hpx = this.parseUnit(h, cn, 'y')
      }
      if (vb) {
        xywh = vb.match(/[-+]?\d+/g).map(parseFloat)
        p.cx = xywh[0]
        p.cy = xywh[1]
        p.width = xywh[2]
        p.height = xywh[3]
        var iw = cn.innerWidth = p.width
        var ih = cn.innerHeight = p.height
        cn.innerSize = Math.sqrt(iw*iw + ih*ih) / Math.sqrt(2)
        if (c.getAttribute('overflow') != 'visible')
          p.clip = true
      }
      if (w) {
        if (vb) { // nuts, let's parse the alignment :|
          var aspect = c.getAttribute('preserveAspectRatio')
          var align = this.parsePreserveAspectRatio(aspect,
            wpx, hpx,
            p.width, p.height)
          p.cx -= align.x / align.w
          p.cy -= align.y / align.h
          p.width = wpx / align.w
          p.height = hpx / align.h
          p.x += align.x
          p.y += align.y
          p.scale = [align.w, align.h]
        }
        // wrong place!
        cn.docWidth = wpx
        cn.docHeight = hpx
      }
      return p
    },

    marker : function(c, cn) {
      var p = new CanvasNode()
      p.draw = function(ctx) {
        if (this.overflow != 'hidden' && this.viewBox) return
        ctx.beginPath()
        ctx.rect(
          this.viewBox[0],this.viewBox[1],
          this.viewBox[2],this.viewBox[3]
        )
        ctx.clip()
      }
      var x = -this.parseUnit(c.getAttribute('refX'), cn, 'x') || 0
      var y = -this.parseUnit(c.getAttribute('refY'), cn, 'y') || 0
      p.transformList = [['translate', [x,y]]]
      p.markerUnits = c.getAttribute('markerUnits') || 'strokeWidth'
      p.markerWidth = this.parseUnit(c.getAttribute('markerWidth'), cn, 'x') || 3
      p.markerHeight = this.parseUnit(
                          c.getAttribute('markerHeight'), cn, 'y') ||
                          3
      p.overflow = c.getAttribute('overflow') || 'hidden'
      p.viewBox = c.getAttribute('viewBox')
      p.orient = c.getAttribute('orient') || 0
      if (p.orient && p.orient != 'auto')
        p.orient = parseFloat(p.orient)*SVGMapping.DEG_TO_RAD_FACTOR
      if (p.viewBox) {
        p.viewBox = p.viewBox.strip().split(/[\s,]+/g).map(parseFloat)
        var vbw = p.viewBox[2] - p.viewBox[0]
        var vbh = p.viewBox[3] - p.viewBox[1]
        if (p.markerWidth) {
          var sx = sy = Math.min(
            p.markerWidth / vbw,
            p.markerHeight / vbh
          )
          p.transformList.unshift(['scale', [sx,sy]])
        }
      }
      return p
    },

    clipPath : function(c,cn) {
      var p = new CanvasNode()
      p.units = c.getAttribute('clipPathUnits')
      return p
    },

    title : function(c, canvasNode) {
      canvasNode.root.title = c.textContent
    },

    desc : function(c,cn) {
      cn.root.description = c.textContent
    },

    metadata : function(c, cn) {
      cn.root.metadata = c
    },








    parseAnimateTag : function(c, cn) {
      var after = SVGParser.SVGTagMapping.parseTime(c.getAttribute('begin'))
      var dur = SVGParser.SVGTagMapping.parseTime(c.getAttribute('dur'))
      var end = SVGParser.SVGTagMapping.parseTime(c.getAttribute('end'))
      if (dur == null) dur = end-after
      dur = isNaN(dur) ? 0 : dur
      var variable = c.getAttribute('attributeName')
      var fill = c.getAttribute('fill')
      if (cn.tagName == 'rect') {
        if (variable == 'x') variable = 'cx'
        if (variable == 'y') variable = 'cy'
      }
      var accum = c.getAttribute('accumulate') == 'sum'
      var additive = c.getAttribute('additive')
      if (additive) additive = additive == 'sum'
      else additive = accum
      var repeat = c.getAttribute('repeatCount')
      if (repeat == 'indefinite') repeat = true
      else repeat = parseFloat(repeat)
      if (!repeat && dur > 0) {
        var repeatDur = c.getAttribute('repeatDur')
        if (repeatDur == 'indefinite') repeat = true
        else repeat = SVGParser.SVGTagMapping.parseTime(repeatDur) / dur
      }
      return {
        after: isNaN(after) ? 0 : after,
        duration: dur,
        restart: c.getAttribute('restart'),
        calcMode : c.getAttribute('calcMode'),
        additive : additive,
        accumulate : accum,
        repeat : repeat,
        variable: variable,
        fill: fill
      }
    },

    parseTime : function(value) {
      if (!value) return null
      if (value.match(/[0-9]$/)) {
        var hms = value.split(":")
        var s = hms[hms.length-1] || 0
        var m = hms[hms.length-2] || 0
        var h = hms[hms.length-3] || 0
        return (parseFloat(h)*3600 + parseFloat(m)*60 + parseFloat(s)) * 1000
      } else {
        var fac = 60
        if (value.match(/s$/i)) fac = 1
        else if (value.match(/h$/i)) fac = 3600
        return parseFloat(value) * fac * 1000
      }
    },






    animate : function(c, cn) {
      var from = this.parseUnit(c.getAttribute('from'), cn, 'x')
      var to = this.parseUnit(c.getAttribute('to'), cn, 'x')
      var by = this.parseUnit(c.getAttribute('by'), cn, 'x')
      var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn)
      if (c.getAttribute('values')) {
        var self = this
        var vals = c.getAttribute('values')
        vals = vals.split(";").map(function(v) {
          var xy = v.split(/[, ]+/)
          if (xy.length > 2) {
            return xy.map(function(x){ return self.parseUnit(x, cn, 'x') })
          } else if (xy.length > 1) {
            return [
              self.parseUnit(xy[0], cn, 'x'),
              self.parseUnit(xy[1], cn, 'y')
            ]
          } else {
            return self.parseUnit(v, cn, 'x')
          }
        })
      } else {
        if (to == null) to = from + by
      }
      cn.after(o.after, function() {
        if (o.fill == 'remove') {
          var orig = Object.clone(this[o.variable])
          this.after(o.duration, function(){ this[o.variable] = orig })
        }
        if (vals) {
          if (o.additive) {
            var ov = this[o.variable]
            vals = vals.map(function(v){
              return Object.sum(v, ov)
            })
          }
          var length = 0
          var lens = []
          if (vals[0] instanceof Array) {
            for (var i=1; i<vals.length; i++) {
              var diff = Object.sub(vals[i] - vals[i-1])
              var sl = Math.sqrt(diff.reduce(function(s, i) { return s + i*i }, 0))
              lens.push(sl)
              length += sl
            }
          } else {
            for (var i=1; i<vals.length; i++) {
              var sl = Math.abs(vals[i] - vals[i-1])
              lens.push(sl)
              length += sl
            }
          }
          var animator = function(pos) {
            if (pos == 1) {
              this[o.variable] = vals[vals.length-1]
            } else {
              if (o.calcMode == 'paced') {
                var len = pos * length
                var rlen = 0, idx, rt
                for (var i=0; i<lens.length; i++) {
                  if (rlen + lens[i] > len) {
                    idx = i
                    rt = (len - rlen) / lens[i]
                    break
                  }
                  rlen += lens[i]
                }
                var v0 = idx
                var v1 = v0 + 1
              } else {
                var idx = pos * (vals.length-1)
                var v0 = Math.floor(idx)
                var rt = idx - v0
                var v1 = v0 + 1
              }
              this.tweenVariable(o.variable, vals[v0], vals[v1], rt, o.calcMode)
            }
          }
          this.animate(animator, from, to, o.duration, 'linear', {
            repeat: o.repeat,
            additive: o.additive,
            accumulate: o.accumulate
          })
        } else {
          if (from == null) {
            from = this[o.variable]
            if (by != null) to = from + by
          }
          if (o.additive) {
            from = Object.sum(from, this[o.variable])
            to = Object.sum(to, this[o.variable])
          }
          this.animate(o.variable, from, to, o.duration, o.calcMode, {
            repeat: o.repeat,
            additive: o.additive,
            accumulate: o.accumulate
          })
        }
      })
    },

    set : function(c, cn) {
      var to = c.getAttribute('to')
      var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn)
      cn.after(o.after, function() {
        if (o.fill == 'remove') {
          var orig = Object.clone(this[o.variable])
          this.after(o.duration, function(){ this[o.variable] = orig })
        }
        this[o.variable] = to
      })
    },

    animateMotion : function(c,cn) {
      var path
      if (c.getAttribute('path')) {
        path = new Path(c.getAttribute('path'))
      } else if (c.getAttribute('values')) {
        var vals = c.getAttribute('values')
        path = new Path("M" + vals.split(";").join("L"))
      } else if (c.getAttribute('from') || c.getAttribute('to') || c.getAttribute('by')) {
        var from = c.getAttribute('from')
        var to = c.getAttribute('to')
        var by = c.getAttribute('by')
        if (!from) from = "0,0"
        if (!to) to = "l" + by
        else to = "L" + to
        path = new Path("M" + from + to)
      }
      var p = new CanvasNode()
      p.__motionPath = path
      var rotate = c.getAttribute('rotate')
      var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn)
      cn.after(o.after, function() {
        if (o.fill == 'remove') {
          var ox = this.x, oy = this.y
          this.after(o.duration, function(){ this.x = ox; this.y = oy})
        }
        var motion = function(pos) {
          var pa = p.__motionPath.pointAngleAt(pos, {
            discrete: o.calcMode == 'discrete',
            linear : o.calcMode == 'linear'
          })
          this.x = pa.point[0]
          this.y = pa.point[1]
          if (rotate == 'auto') {
            this.rotation = pa.angle
          } else if (rotate == 'auto-reverse') {
            this.rotation = pa.angle + Math.PI
          }
        }
        this.animate(motion, 0, 1, o.duration, 'linear', {
          repeat: o.repeat,
          additive: o.additive,
          accumulate: o.accumulate
        })
      })
      return p
    },

    mpath : function(c,cn, defs) {
      var href = c.getAttribute('xlink:href')
      href = href.replace(/^#/,'')
      this.getDef(defs, href, function(obj) {
        cn.__motionPath = obj
      })
    },

    animateColor : function(c, cn, defs) {
      var from = c.getAttribute('from')
      var to = c.getAttribute('to')
      from = SVGParser.SVGMapping.__parseStyle(from, null, defs)
      to = SVGParser.SVGMapping.__parseStyle(to, null, defs)
      var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn)
      cn.after(o.after, function() {
        if (o.fill == 'remove') {
          var orig = Object.clone(this[o.variable])
          this.after(o.duration, function(){ this[o.variable] = orig })
        }
        this.animate(o.variable, from, to, o.duration, 'linear', {
          repeat: o.repeat,
          additive: o.additive,
          accumulate: o.accumulate
        })
      })
    },

    animateTransform : function(c, cn) {
      var from = c.getAttribute('from')
      var to = c.getAttribute('to')
      var by = c.getAttribute('by')
      var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn)
      if (from) from = from.split(/[ ,]+/).map(parseFloat)
      if (to) to = to.split(/[ ,]+/).map(parseFloat)
      if (by) by = by.split(/[ ,]+/).map(parseFloat)
      o.variable = c.getAttribute('type')
      if (o.variable == 'rotate') {
        o.variable = 'rotation'
        if (from) from = from.map(function(v) { return v * Math.PI/180 })
        if (to) to = to.map(function(v) { return v * Math.PI/180 })
        if (by) by = by.map(function(v) { return v * Math.PI/180 })
      } else if (o.variable.match(/^skew/)) {
        if (from) from = from.map(function(v) { return v * Math.PI/180 })
        if (to) to = to.map(function(v) { return v * Math.PI/180 })
        if (by) by = by.map(function(v) { return v * Math.PI/180 })
      }
      if (to == null) to = Object.sum(from, by)
      cn.after(o.after, function() {
        if (o.variable == 'translate') {
          if (from == null) {
            from = [this.x, this.y]
            if (by != null) to = Object.sum(from, by)
          }
          if (o.fill == 'remove') {
            var ox = this.x
            var oy = this.y
            this.after(o.duration, function(){ this.x = ox; this.y = oy })
          }
          this.animate('x', from[0], to[0], o.duration, 'linear', {
            repeat: o.repeat,
            additive: o.additive,
            accumulate: o.accumulate
          })
          if (from[1] != null) {
            this.animate('y', from[1], to[1], o.duration, 'linear', {
              repeat: o.repeat,
              additive: o.additive,
              accumulate: o.accumulate
            })
          }
        } else {
          if (from) {
            if (from.length == 1) from = from[0]
          }
          if (to) {
            if (to.length == 1) to = to[0]
          }
          if (by) {
            if (by.length == 1) by = by[0]
          }
          if (from == null) {
            from = this[o.variable]
            if (by != null) to = Object.sum(from, by)
          }
          if (o.variable == 'scale' && o.additive) {
            // +1 in SMIL's additive scale means *1, welcome to brokenville
            o.additive = false
          }
          if (o.fill == 'remove') {
            var orig = Object.clone(this[o.variable])
            this.after(o.duration, function(){ this[o.variable] = orig })
          }
          this.animate(o.variable, from, to, o.duration, 'linear', {
            repeat: o.repeat,
            additive: o.additive,
            accumulate: o.accumulate
          })
        }
      })
    },

    a : function(c, cn) {
      var href = c.getAttribute('xlink:href') ||
                 c.getAttribute('href')
      var target = c.getAttribute('target')
      var p = new LinkNode(href, target)
      return p
    },

    use : function(c, cn, defs, style) {
      var id = c.getAttribute('xlink:href') ||
                c.getAttribute('href')
      var p = new CanvasNode()
      if (id) {
        id = id.replace(/^#/,'')
        this.getDef(defs, id, function(obj) {
          var oc = obj.clone()
          var par = p.parent
          if (par) {
            if (p.stroke) oc.stroke = p.stroke
            if (p.fill) oc.fill = p.fill
            p.append(oc)
          } else {
            p = oc
          }
        })
      }
      return p
    },

    image : function(c, cn, defs, style) {
      var src = c.getAttribute('xlink:href') ||
                c.getAttribute('href')
      if (src && src.search(/^[a-z]+:/i) != 0) {
        src = cn.root.filename.split("/").slice(0,-1).join("/") + "/" + src
      }
      var p = new ImageNode(src ? Object.loadImage(src) : null)
      p.fill = 'none'
      p.dX = this.parseUnit(c.getAttribute('x'), cn, 'x') || 0
      p.dY = this.parseUnit(c.getAttribute('y'), cn, 'y') || 0
      p.srcWidth = this.parseUnit(c.getAttribute('width'), cn, 'x')
      p.srcHeight = this.parseUnit(c.getAttribute('height'), cn, 'y')
      return p
    },

    path : function(c) {
      return new Path(c.getAttribute("d"))
    },

    polygon : function(c) {
      return new Polygon(c.getAttribute("points").toString().strip()
                          .split(/[\s,]+/).map(parseFloat))
    },

    polyline : function(c) {
      return new Polygon(c.getAttribute("points").toString().strip()
                          .split(/[\s,]+/).map(parseFloat), {closePath:false})
    },

    rect : function(c, cn) {
      var p = new Rectangle(
        this.parseUnit(c.getAttribute('width'), cn, 'x'),
        this.parseUnit(c.getAttribute('height'), cn, 'y')
      )
      p.cx = this.parseUnit(c.getAttribute('x'), cn, 'x') || 0
      p.cy = this.parseUnit(c.getAttribute('y'), cn, 'y') || 0
      p.rx = this.parseUnit(c.getAttribute('rx'), cn, 'x') || 0
      p.ry = this.parseUnit(c.getAttribute('ry'), cn, 'y') || 0
      return p
    },

    line : function(c, cn) {
      var x1 = this.parseUnit(c.getAttribute('x1'), cn, 'x') || 0
      var y1 = this.parseUnit(c.getAttribute('y1'), cn, 'y') || 0
      var x2 = this.parseUnit(c.getAttribute('x2'), cn, 'x') || 0
      var y2 = this.parseUnit(c.getAttribute('y2'), cn, 'y') || 0
      var p = new Line(x1,y1, x2,y2)
      return p
    },

    circle : function(c, cn) {
      var p = new Circle(this.parseUnit(c.getAttribute('r'), cn) || 0)
      p.cx = this.parseUnit(c.getAttribute('cx'), cn, 'x') || 0
      p.cy = this.parseUnit(c.getAttribute('cy'), cn, 'y') || 0
      return p
    },

    ellipse : function(c, cn) {
      var p = new Ellipse(
        this.parseUnit(c.getAttribute('rx'), cn, 'x') || 0,
        this.parseUnit(c.getAttribute('ry'), cn, 'y') || 0
      )
      p.cx = this.parseUnit(c.getAttribute('cx'), cn, 'x') || 0
      p.cy = this.parseUnit(c.getAttribute('cy'), cn, 'y') || 0
      return p
    },

    text : function(c, cn) {
      if (false) {
        var p = new TextNode(c.textContent.strip())
        p.setAsPath(true)
        p.cx = this.parseUnit(c.getAttribute('x'),cn, 'x') || 0
        p.cy = this.parseUnit(c.getAttribute('y'),cn, 'y') || 0
        return p
      } else {
        var e = E('div', c.textContent.strip())
        e.style.marginTop = '-1em'
        e.style.whiteSpace = 'nowrap'
        var p = new ElementNode(e)
        p.xOffset = this.parseUnit(c.getAttribute('x'),cn, 'x') || 0
        p.yOffset = this.parseUnit(c.getAttribute('y'),cn, 'y') || 0
        return p
      }
    },

    style : function(c, cn, defs, style) {
      this.parseStyle(c, style)
    },

    defs : function(c, cn, defs, style) {
      return new CanvasNode({visible: false})
    },

    linearGradient : function(c, cn,defs,style) {
      var g = new Gradient({type:'linear'})
      g.color = cn.color
      if (c.getAttribute('color')) {
        SVGParser.SVGMapping.color(g, c.getAttribute('color'), defs, style)
      }
      g.svgNode = c
      var x1 = c.getAttribute('x1')
      var y1 = c.getAttribute('y1')
      var x2 = c.getAttribute('x2')
      var y2 = c.getAttribute('y2')
      var transform = c.getAttribute('gradientTransform')
      g.units = c.getAttribute('gradientUnits') || "objectBoundingBox"
      if (x1) g.startX = parseFloat(x1) * (x1.charAt(x1.length-1) == '%' ? 0.01 : 1)
      if (y1) g.startY = parseFloat(y1) * (y1.charAt(y1.length-1) == '%' ? 0.01 : 1)
      if (x2) g.endX = parseFloat(x2) * (x2.charAt(x2.length-1) == '%' ? 0.01 : 1)
      if (y2) g.endY = parseFloat(y2) * (y2.charAt(y2.length-1) == '%' ? 0.01 : 1)
      if (transform) this.applySVGTransform(g, transform, defs, style)
      this.parseStops(g, c, defs, style)
      return g
    },

    radialGradient : function(c, cn, defs, style) {
      var g = new Gradient({type:'radial'})
      g.color = cn.color
      if (c.getAttribute('color')) {
        SVGParser.SVGMapping.color(g, c.getAttribute('color'), defs, style)
      }
      g.svgNode = c
      var r = c.getAttribute('r')
      var fx = c.getAttribute('fx')
      var fy = c.getAttribute('fy')
      var cx = c.getAttribute('cx')
      var cy = c.getAttribute('cy')
      var transform = c.getAttribute('gradientTransform')
      g.units = c.getAttribute('gradientUnits') || "objectBoundingBox"
      if (r) g.endRadius = parseFloat(r) * (r.charAt(r.length-1) == '%' ? 0.01 : 1)
      if (fx) g.startX = parseFloat(fx) * (fx.charAt(fx.length-1) == '%' ? 0.01 : 1)
      if (fy) g.startY = parseFloat(fy) * (fy.charAt(fy.length-1) == '%' ? 0.01 : 1)
      if (cx) g.endX = parseFloat(cx) * (cx.charAt(cx.length-1) == '%' ? 0.01 : 1)
      if (cy) g.endY = parseFloat(cy) * (cy.charAt(cy.length-1) == '%' ? 0.01 : 1)
      if (transform) this.applySVGTransform(g, transform, defs, style)
      this.parseStops(g, c, defs, style)
      return g
    }
  },

  parseChildren : function(node, canvasNode, defs, style) {
    var childNodes = []
    var cn = canvasNode
    for (var i=0; i<node.childNodes.length; i++) {
      var c = node.childNodes[i]
      var p = false // argh, remember to initialize vars inside loops
      if (c.childNodes) {
        if (c.tagName) {
          if (this.SVGTagMapping[c.tagName]) {
            p = this.SVGTagMapping[c.tagName].call(
                  this, c, canvasNode, defs, style)
          } else {
            p = new CanvasNode()
          }
          if (p) {
            p.root = canvasNode.root
            p.fontSize = cn.fontSize
            p.strokeWidth = cn.strokeWidth
            if (c.attributes) {
              for (var j=0; j<c.attributes.length; j++) {
                var attr = c.attributes[j]
                if (this.SVGMapping[attr.nodeName])
                  this.SVGMapping[attr.nodeName](p, attr.nodeValue, defs, style)
              }
            }
            if (p.id) {
              this.setDef(defs, p.id, p)
            }
            p.tagName = c.tagName
            this.applySVGTransform(p, c.getAttribute("transform"), defs, style)
            this.applySVGStyle(p, c.getAttribute("style"), defs, style)
            if (p.tagName && style.tags[p.tagName])
              this.applySVGStyle(p, style.tags[p.tagName], defs, style)
            if (p.className && style.classes[p.className])
              this.applySVGStyle(p, style.classes[p.className], defs, style)
            if (p.id && style.ids[p.id])
              this.applySVGStyle(p, style.ids[p.id], defs, style)
            if (!p.marker) p.marker = cn.marker
            if (!p.markerStart) p.markerStart = cn.markerStart
            if (!p.markerEnd) p.markerEnd = cn.markerEnd
            if (!p.markerMid) p.markerMid = cn.markerMid
          }
        }
        if (p && p.setRoot) {
          p.zIndex = i
          canvasNode.append(p)
          this.parseChildren(c, p, defs, style)
        }
      }
    }
  },

  parseStyle : function(node, style) {
    var text = node.textContent
    var segs = text.split(/\}/m)
    for (var i=0; i<segs.length; i++) {
      var seg = segs[i]
      var kv = seg.split(/\{/m)
      if (kv.length < 2) continue
      var key = kv[0].strip()
      var value = kv[1].strip()
      switch (key.charAt(0)) {
        case '.':
          style.classes[key.slice(1)] = value
          break;
        case '#':
          style.ids[key.slice(1)] = value
          break;
        default:
          style.tags[key] = value
          break;
      }
    }
  },

  parseStops : function(g, node, defs, style) {
    var href = node.getAttribute('xlink:href')
    g.colorStops = []
    if (href) {
      href = href.replace(/^#/,'')
      this.getDef(defs, href, function(g2) {
        if (g.colorStops.length == 0)
          g.colorStops = g2.colorStops
      })
    }
    var stops = []
    for (var i=0; i<node.childNodes.length; i++) {
      var c = node.childNodes[i]
      if (c.tagName == 'stop') {
        var offset = parseFloat(c.getAttribute('offset'))
        if (c.getAttribute('offset').search(/%/) != -1)
          offset *= 0.01
        var stop = [offset]
        stop.color = g.color
        for (var j=0; j<c.attributes.length; j++) {
          var attr = c.attributes[j]
          if (this.SVGMapping[attr.nodeName])
            this.SVGMapping[attr.nodeName](stop, attr.nodeValue, defs, style)
        }
        this.applySVGStyle(stop, c.getAttribute('style'), defs, style)
        var id = c.getAttribute('id')
        if (id) this.setDef(defs, id, stop)
        stops.push(stop)
      }
    }
    if (stops.length > 0)
      g.colorStops = stops
  },

  applySVGTransform : function(node, transform, defs, style) {
    if (!transform) return
    node.transformList = []
    var segs = transform.match(/[a-z]+\s*\([^)]*\)/ig)
    for (var i=0; i<segs.length; i++) {
      var kv = segs[i].split("(")
      var k = kv[0].strip()
      if (this.SVGMapping[k]) {
        var v = kv[1].strip().slice(0,-1)
        this.SVGMapping[k](node, v, defs, style)
      }
    }
    this.breakDownTransformList(node)
  },

  breakDownTransformList : function(node) {
    var tl = node.transformList
    if (node.transformList.length == 1) {
      var tr = tl[0]
      if (tr[0] == 'translate') {
        node.x = tr[1][0]
        node.y = tr[1][1]
      } else if (tr[0] == 'scale') {
        node.scale = tr[1]
      } else if (tr[0] == 'rotate') {
        node.rotation = tr[1]
      } else if (tr[0] == 'matrix') {
        node.matrix = tr[1]
      } else if (tr[0] == 'skewX') {
        node.skewX = tr[1][0]
      } else if (tr[0] == 'skewY') {
        node.skewY = tr[1][0]
      } else {
        return
      }
      node.transformList = null
    }
  },

  applySVGStyle : function(node, style, defs, st) {
    if (!style) return
    var segs = style.split(";")
    for (var i=0; i<segs.length; i++) {
      var kv = segs[i].split(":")
      var k = kv[0].strip()
      if (this.SVGMapping[k]) {
        var v = kv[1].strip()
        this.SVGMapping[k](node, v, defs, st)
      }
    }
  },

  getDef : function(defs, id, f) {
    if (defs[id] && defs[id] instanceof Array) {
      defs[id].push(f)
    } else if (defs[id]) {
      f(defs[id])
    } else {
      defs[id] = [f]
    }
  },

  setDef : function(defs, id, obj) {
    if (defs[id] && defs[id] instanceof Array) {
      for (var i=0; i<defs[id].length; i++) {
        defs[id][i](obj)
      }
    }
    defs[id] = obj
  },

  parseUnit : function(v, parent, dir) {
    if (v == null) {
      return null
    } else {
      return this.parseUnitMultiplier(v, parent, dir) * parseFloat(v.strip())
    }
  },

  parseUnitMultiplier : function(str, parent, dir) {
    var cm = this.getCmInPixels()
    if (str.search(/cm$/i) != -1)
      return cm
    else if (str.search(/mm$/i) != -1)
      return 0.1 * cm
    else if (str.search(/pt$/i) != -1)
      return 0.0352777778 * cm
    else if (str.search(/pc$/i) != -1)
      return 0.4233333333 * cm
    else if (str.search(/in$/i) != -1)
      return 2.54 * cm
    else if (str.search(/em$/i) != -1)
      return parent.fontSize
    else if (str.search(/ex$/i) != -1)
      return parent.fontSize / 2
    else if (str.search(/%$/i) != -1)
      if (dir == 'x')
        return parent.root.innerWidth * 0.01
      else if (dir == 'y')
        return parent.root.innerHeight * 0.01
      else
        return parent.root.innerSize * 0.01
    else
      return 1
  },

  getCmInPixels : function() {
    if (!this.cmInPixels) {
      var e = E('div',{ style: {
        margin: '0px',
        padding: '0px',
        width: '1cm',
        height: '1cm',
        position: 'absolute',
        visibility: 'hidden'
      }})
      document.body.appendChild(e)
      var cm = e.offsetWidth
      document.body.removeChild(e)
      this.cmInPixels = cm || 38
    }
    return this.cmInPixels
  },

  getEmInPixels : function() {
    if (!this.emInPixels) {
      var e = E('div',{ style: {
        margin: '0px',
        padding: '0px',
        width: '1em',
        height: '1em',
        position: 'absolute',
        visibility: 'hidden'
      }})
      document.body.appendChild(e)
      var em = e.offsetWidth
      document.body.removeChild(e)
      this.emInPixels = em || 12
    }
    return this.emInPixels
  },

  getExInPixels : function() {
    if (!this.exInPixels) {
      var e = E('div',{ style: {
        margin: '0px',
        padding: '0px',
        width: '1ex',
        height: '1ex',
        position: 'absolute',
        visibility: 'hidden'
      }})
      document.body.appendChild(e)
      var ex = e.offsetWidth
      document.body.removeChild(e)
      this.exInPixels = ex || 6
    }
    return this.exInPixels
  },

  SVGMapping : {
    DEG_TO_RAD_FACTOR : Math.PI / 180,
    RAD_TO_DEG_FACTOR : 180 / Math.PI,

    parseUnit : function(v, cn, dir) {
      return SVGParser.parseUnit(v, cn, dir)
    },

    "class" : function(node, v) {
      node.className = v
    },

    marker : function(node, v, defs) {
      SVGParser.getDef(defs, v.replace(/^url\(#|\)$/g, ''), function(g) {
        node.marker = g
      })
    },

    "marker-start" : function(node, v, defs) {
      SVGParser.getDef(defs, v.replace(/^url\(#|\)$/g, ''), function(g) {
        node.markerStart = g
      })
    },

    "marker-end" : function(node, v, defs) {
      SVGParser.getDef(defs, v.replace(/^url\(#|\)$/g, ''), function(g) {
        node.markerEnd = g
      })
    },

    "marker-mid" : function(node, v, defs) {
      SVGParser.getDef(defs, v.replace(/^url\(#|\)$/g, ''), function(g) {
        node.markerMid = g
      })
    },

    "clip-path" : function(node, v, defs) {
      SVGParser.getDef(defs, v.replace(/^url\(#|\)$/g, ''), function(g) {
        node.clipPath = g
      })
    },

    id : function(node, v) {
      node.id = v
    },

    translate : function(node, v) {
      var xy = v.split(/[\s,]+/).map(parseFloat)
      node.transformList.push(['translate', [xy[0], xy[1] || 0]])
    },

    rotate : function(node, v) {
      if (v == 'auto' || v == 'auto-reverse') return
      var rot = v.split(/[\s,]+/).map(parseFloat)
      var angle = rot[0] * this.DEG_TO_RAD_FACTOR
      if (rot.length > 1)
        node.transformList.push(['rotate', [angle, rot[1], rot[2] || 0]])
      else
        node.transformList.push(['rotate', [angle]])
    },

    scale : function(node, v) {
      var xy = v.split(/[\s,]+/).map(parseFloat)
      var trans = ['scale']
      if (xy.length > 1)
        trans[1] = [xy[0], xy[1]]
      else
        trans[1] = [xy[0], xy[0]]
      node.transformList.push(trans)
    },

    matrix : function(node, v) {
      var mat = v.split(/[\s,]+/).map(parseFloat)
      node.transformList.push(['matrix', mat])
    },

    skewX : function(node, v) {
      var angle = parseFloat(v)*this.DEG_TO_RAD_FACTOR
      node.transformList.push(['skewX', [angle]])
    },

    skewY : function(node, v) {
      var angle = parseFloat(v)*this.DEG_TO_RAD_FACTOR
      node.transformList.push(['skewY', [angle]])
    },

    opacity : function(node, v) {
      node.opacity = parseFloat(v)
    },

    display : function (node, v) {
      node.display = v
    },

    visibility : function (node, v) {
      node.visibility = v
    },

    'stroke-miterlimit' : function(node, v) {
      node.miterLimit = parseFloat(v)
    },

    'stroke-linecap' : function(node, v) {
      node.lineCap = v
    },

    'stroke-linejoin' : function(node, v) {
      node.lineJoin = v
    },

    'stroke-width' : function(node, v) {
      node.strokeWidth = this.parseUnit(v, node)
    },

    fill : function(node, v, defs, style) {
      node.fill = this.__parseStyle(v, node.fill, defs, node.color)
    },

    stroke : function(node, v, defs, style) {
      node.stroke = this.__parseStyle(v, node.stroke, defs, node.color)
    },

    color : function(node, v, defs, style) {
      if (v == 'inherit') return
      node.color = this.__parseStyle(v, false, defs, node.color)
    },

    'stop-color' : function(node, v, defs, style) {
      if (v == 'none') {
        node[1] = [0,0,0,0]
      } else {
        node[1] = this.__parseStyle(v, node[1], defs, node.color)
      }
    },

    'fill-opacity' : function(node, v) {
      node.fillOpacity = Math.min(1,Math.max(0,parseFloat(v)))
    },

    'stroke-opacity' : function(node, v) {
      node.strokeOpacity = Math.min(1,Math.max(0,parseFloat(v)))
    },

    'stop-opacity' : function(node, v) {
      node[1] = node[1] || [0,0,0]
      node[1][3] = Math.min(1,Math.max(0,parseFloat(v)))
    },

    'text-anchor' : function(node, v) {
      node.textAnchor = v
      if (node.setAlign) {
        if (v == 'middle')
          node.setAlign('center')
        else
          node.setAlign(v)
      }
    },

    'font-family' : function(node, v) {
      node.fontFamily = v
    },

    'font-size' : function(node, v) {
      node.fontSize = this.parseUnit(v, node)
    },

    __parseStyle : function(v, currentStyle, defs, currentColor) {

      if (v.charAt(0) == '#') {
        if (v.length == 4)
          v = v.replace(/([^#])/g, '$1$1')
        var a = v.slice(1).match(/../g).map(
          function(i) { return parseInt(i, 16) })
        return a

      } else if (v.search(/^rgb\(/) != -1) {
        var a = v.slice(4,-1).split(",")
        for (var i=0; i<a.length; i++) {
          var c = a[i].strip()
          if (c.charAt(c.length-1) == '%')
            a[i] = Math.round(parseFloat(c.slice(0,-1)) * 2.55)
          else
            a[i] = parseInt(c)
        }
        return a

      } else if (v.search(/^rgba\(/) != -1) {
        var a = v.slice(5,-1).split(",")
        for (var i=0; i<3; i++) {
          var c = a[i].strip()
          if (c.charAt(c.length-1) == '%')
            a[i] = Math.round(parseFloat(c.slice(0,-1)) * 2.55)
          else
            a[i] = parseInt(c)
        }
        var c = a[3].strip()
        if (c.charAt(c.length-1) == '%')
          a[3] = Math.round(parseFloat(c.slice(0,-1)) * 0.01)
        else
          a[3] = Math.max(0, Math.min(1, parseFloat(c)))
        return a

      } else if (v.search(/^url\(/) != -1) {
        var id = v.match(/\([^)]+\)/)[0].slice(1,-1).replace(/^#/, '')
        if (defs[id]) {
          return defs[id]
        } else { // missing defs, let's make it known that we're screwed
          return 'rgba(255,0,255,1)'
        }

      } else if (v == 'currentColor') {
        return currentColor

      } else if (v == 'none') {
        return 'none'

      } else if (v == 'freeze') { // SMIL is evil, but so are we
        return null

      } else if (v == 'remove') {
        return null

      } else { // unknown value, maybe it's an ICC color
        return v
      }
    }
  }
}

