--- /dev/null
+/*
+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)