1 /*
  2 CAKE - Canvas Animation Kit Experiment
  3 
  4 Copyright (C) 2007  Ilmari Heikkinen
  5 
  6 Permission is hereby granted, free of charge, to any person
  7 obtaining a copy of this software and associated documentation
  8 files (the "Software"), to deal in the Software without
  9 restriction, including without limitation the rights to use,
 10 copy, modify, merge, publish, distribute, sublicense, and/or sell
 11 copies of the Software, and to permit persons to whom the
 12 Software is furnished to do so, subject to the following
 13 conditions:
 14 
 15 The above copyright notice and this permission notice shall be
 16 included in all copies or substantial portions of the Software.
 17 
 18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 20 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 21 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 22 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 23 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 24 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 25 OTHER DEALINGS IN THE SOFTWARE.
 26 */
 27 
 28 
 29 /**
 30   Delete the first instance of obj from the array.
 31 
 32   @param obj The object to delete
 33   @return true on success, false if array contains no instances of obj
 34   @type boolean
 35   @addon
 36   */
 37 Array.prototype.deleteFirst = function(obj) {
 38   for (var i=0; i<this.length; i++) {
 39     if (this[i] == obj) {
 40       this.splice(i,1)
 41       return true
 42     }
 43   }
 44   return false
 45 }
 46 
 47 
 48 /**
 49   Compares two arrays for equality. Returns true if the arrays are equal.
 50   */
 51 Array.prototype.equals = function(array) {
 52   if (!array) return false
 53   if (this.length != array.length) return false
 54   for (var i=0; i<this.length; i++) {
 55     var a = this[i]
 56     var b = array[i]
 57     if (a.equals && typeof(a.equals) == 'function') {
 58       if (!a.equals(b)) return false
 59     } else if (a != b) {
 60       return false
 61     }
 62   }
 63   return true
 64 }
 65 
 66 /**
 67   Rotates the first element of an array to be the last element.
 68   Rotates last element to be the first element when backToFront is true.
 69 
 70   @param {boolean} backToFront Whether to move the last element to the front or not
 71   @return The last element when backToFront is false, the first element when backToFront is true
 72   @addon
 73   */
 74 Array.prototype.rotate = function(backToFront) {
 75   if (backToFront) {
 76     this.unshift(this.pop())
 77     return this[0]
 78   } else {
 79     this.push(this.shift())
 80     return this[this.length-1]
 81   }
 82 }
 83 /**
 84   Returns a random element from the array.
 85 
 86   @return A random element
 87   @addon
 88  */
 89 Array.prototype.pick = function() {
 90   return this[Math.floor(Math.random()*this.length)]
 91 }
 92 
 93 Array.prototype.flatten = function() {
 94   var a = []
 95   for (var i=0; i<this.length; i++) {
 96     var e = this[i]
 97     if (e.flatten) {
 98       var ef = e.flatten()
 99       for (var j=0; j<ef.length; j++) {
100         a[a.length] = ef[j]
101       }
102     } else {
103       a[a.length] = e
104     }
105   }
106   return a
107 }
108 
109 Array.prototype.take = function() {
110   var a = []
111   for (var i=0; i<this.length; i++) {
112     var e = []
113     for (var j=0; j<arguments.length; j++) {
114       e[j] = this[i][arguments[j]]
115     }
116     a[i] = e
117   }
118   return a
119 }
120 
121 if (!Array.prototype.pluck) {
122   Array.prototype.pluck = function(key) {
123     var a = []
124     for (var i=0; i<this.length; i++) {
125       a[i] = this[i][key]
126     }
127     return a
128   }
129 }
130 
131 Array.prototype.set = function(key, value) {
132   for (var i=0; i<this.length; i++) {
133     this[i][key] = value
134   }
135 }
136 
137 Array.prototype.allWith = function() {
138   var a = []
139   topLoop:
140   for (var i=0; i<this.length; i++) {
141     var e = this[i]
142     for (var j=0; j<arguments.length; j++) {
143       if (!this[i][arguments[j]])
144         continue topLoop
145     }
146     a[a.length] = e
147   }
148   return a
149 }
150 
151 Element.prototype.append = function() {
152   for(var i=0; i<arguments.length; i++) {
153     if (typeof(arguments[i]) == 'string') {
154       this.appendChild(T(arguments[i]))
155     } else {
156       this.appendChild(arguments[i])
157     }
158   }
159 }
160 
161 // some common helper methods
162 
163 if (!Function.prototype.bind) {
164   /**
165     Creates a function that calls this function in the scope of the given
166     object.
167 
168       var obj = { x: 'obj' }
169       var f = function() { return this.x }
170       window.x = 'window'
171       f()
172       // => 'window'
173       var g = f.bind(obj)
174       g()
175       // => 'obj'
176 
177     @param object Object to bind this function to
178     @return Function bound to object
179     @addon
180     */
181   Function.prototype.bind = function(object) {
182     var t = this
183     return function() {
184       return t.apply(object, arguments)
185     }
186   }
187 }
188 
189 if (!Array.prototype.last) {
190   /**
191     Returns the last element of the array.
192 
193     @return The last element of the array
194     @addon
195     */
196   Array.prototype.last = function() {
197     return this[this.length-1]
198   }
199 }
200 if (!Array.prototype.indexOf) {
201   /**
202     Returns the index of obj if it is in the array.
203     Returns -1 otherwise.
204 
205     @param obj The object to find from the array.
206     @return The index of obj or -1 if obj isn't in the array.
207     @addon
208     */
209   Array.prototype.indexOf = function(obj) {
210     for (var i=0; i<this.length; i++)
211       if (obj == this[i]) return i
212     return -1
213   }
214 }
215 /**
216   Iterate function f over each element of the array and return an array
217   of the return values.
218 
219   @param f Function to apply to each element
220   @return An array of return values from applying f on each element of the array
221   @type Array
222   @addon
223   */
224 Array.prototype.map = function(f) {
225   var na = new Array(this.length)
226   if (f)
227     for (var i=0; i<this.length; i++) na[i] = f(this[i], i, this)
228   else
229     for (var i=0; i<this.length; i++) na[i] = this[i]
230   return na
231 }
232 Array.prototype.forEach = function(f) {
233   for (var i=0; i<this.length; i++) f(this[i], i, this)
234 }
235 if (!Array.prototype.reduce) {
236   Array.prototype.reduce = function(f, s) {
237     var i = 0
238     if (arguments.length == 1) {
239       s = this[0]
240       i++
241     }
242     for(; i<this.length; i++) {
243       s = f(s, this[i], i, this)
244     }
245     return s
246   }
247 }
248 if (!Array.prototype.find) {
249   Array.prototype.find = function(f) {
250     for(var i=0; i<this.length; i++) {
251       if (f(this[i], i, this)) return this[i]
252     }
253   }
254 }
255 
256 if (!String.prototype.capitalize) {
257   /**
258     Returns a copy of this string with the first character uppercased.
259 
260     @return Capitalized version of the string
261     @type String
262     @addon
263     */
264   String.prototype.capitalize = function() {
265     return this.replace(/^./, this.slice(0,1).toUpperCase())
266   }
267 }
268 
269 if (!String.prototype.escape) {
270   /**
271     Returns a version of the string that can be used as a string literal.
272 
273     @return Copy of string enclosed in double-quotes, with double-quotes
274             inside string escaped.
275     @type String
276     @addon
277     */
278   String.prototype.escape = function() {
279     return '"' + this.replace(/"/g, '\\"') + '"'
280   }
281 }
282 if (!String.prototype.splice) {
283   String.prototype.splice = function(start, count, replacement) {
284     return this.slice(0,start) + replacement + this.slice(start+count)
285   }
286 }
287 if (!String.prototype.strip) {
288   /**
289     Returns a copy of the string with preceding and trailing whitespace
290     removed.
291 
292     @return Copy of string sans surrounding whitespace.
293     @type String
294     @addon
295     */
296   String.prototype.strip = function() {
297     return this.replace(/^\s+|\s+$/g, '')
298   }
299 }
300 
301 if (!window['$A']) {
302   /**
303     Creates a new array from an object with #length.
304     */
305   $A = function(obj) {
306     var a = new Array(obj.length)
307     for (var i=0; i<obj.length; i++)
308       a[i] = obj[i]
309     return a
310   }
311 }
312 
313 if (!window['$']) {
314   $ = function(id) {
315     return document.getElementById(id)
316   }
317 }
318 
319 if (!Math.sinh) {
320   /**
321     Returns the hyperbolic sine of x.
322 
323     @param x The value for x
324     @return The hyperbolic sine of x
325     @addon
326     */
327   Math.sinh = function(x) {
328     return 0.5 * (Math.exp(x) - Math.exp(-x))
329   }
330   /**
331     Returns the inverse hyperbolic sine of x.
332 
333     @param x The value for x
334     @return The inverse hyperbolic sine of x
335     @addon
336     */
337   Math.asinh = function(x) {
338     return Math.log(x + Math.sqrt(x*x + 1))
339   }
340 }
341 if (!Math.cosh) {
342   /**
343     Returns the hyperbolic cosine of x.
344 
345     @param x The value for x
346     @return The hyperbolic cosine of x
347     @addon
348     */
349   Math.cosh = function(x) {
350     return 0.5 * (Math.exp(x) + Math.exp(-x))
351   }
352   /**
353     Returns the inverse hyperbolic cosine of x.
354 
355     @param x The value for x
356     @return The inverse hyperbolic cosine of x
357     @addon
358     */
359   Math.acosh = function(x) {
360     return Math.log(x + Math.sqrt(x*x - 1))
361   }
362 }
363 
364 /**
365   Creates and configures a DOM element.
366 
367   The tag of the element is given by name.
368 
369   If params is a string, it is used as the innerHTML of the created element.
370   If params is a DOM element, it is appended to the created element.
371   If params is an object, it is treated as a config object and merged
372   with the created element.
373 
374   If params is a string or DOM element, the third argument is treated
375   as the config object.
376 
377   Special attributes of the config object:
378     * content
379       - if content is a string, it is used as the innerHTML of the
380         created element
381       - if content is an element, it is appended to the created element
382     * style
383       - the style object is merged with the created element's style
384 
385   @param {String} name The tag for the created element
386   @param params The content or config for the created element
387   @param config The config for the created element if params is content
388   @return The created DOM element
389   */
390 E = function(name, params, config) {
391   var el = document.createElement(name)
392   if (params) {
393     if (typeof(params) == 'string') {
394       el.innerHTML = params
395       params = config
396     } else if (params.DOCUMENT_NODE) {
397       el.appendChild(params)
398       params = config
399     }
400     if (params) {
401       if (params.style) {
402         var style = params.style
403         params = Object.clone(params)
404         delete params.style
405         Object.forceExtend(el.style, style)
406       }
407       if (params.content) {
408         if (typeof(params.content) == 'string') {
409           el.appendChild(T(params.content))
410         } else {
411           el.appendChild(params.content)
412         }
413         params = Object.clone(params)
414         delete params.content
415       }
416       Object.forceExtend(el, params)
417     }
418   }
419   return el
420 }
421 // Safari requires each canvas to have a unique id.
422 E.lastCanvasId = 0
423 /**
424   Creates and returns a canvas element with width w and height h.
425 
426   @param {int} w The width for the canvas
427   @param {int} h The height for the canvas
428   @param config Optional config object to pass to E()
429   @return The created canvas element
430   */
431 E.canvas = function(w,h,config) {
432   var id = 'canvas-uuid-' + E.lastCanvasId
433   E.lastCanvasId++
434   if (!config) config = {}
435   return E('canvas', Object.extend(config, {id: id, width: w, height: h}))
436 }
437 
438 /**
439   Shortcut for document.createTextNode.
440 
441   @param {String} text The text for the text node
442   @return The created text node
443   */
444 T = function(text) {
445   return document.createTextNode(text)
446 }
447 
448 /**
449   Merges the src object's attributes with the dst object, ignoring errors.
450 
451   @param dst The destination object
452   @param src The source object
453   @return The dst object
454   @addon
455   */
456 Object.forceExtend = function(dst, src) {
457   for (var i in src) {
458     try{ dst[i] = src[i] } catch(e) {}
459   }
460   return dst
461 }
462 // In case Object.extend isn't defined already, set it to Object.forceExtend.
463 if (!Object.extend)
464   Object.extend = Object.forceExtend
465 
466 /**
467   Merges the src object's attributes with the dst object, preserving all dst
468   object's current attributes.
469 
470   @param dst The destination object
471   @param src The source object
472   @return The dst object
473   @addon
474   */
475 Object.conditionalExtend = function(dst, src) {
476   for (var i in src) {
477     if (dst[i] == null)
478       dst[i] = src[i]
479   }
480   return dst
481 }
482 
483 /**
484   Creates and returns a shallow copy of the src object.
485 
486   @param src The source object
487   @return A clone of the src object
488   @addon
489   */
490 Object.clone = function(src) {
491   if (!src || src == true)
492     return src
493   switch (typeof(src)) {
494     case 'string':
495       return Object.extend(src+'', src)
496       break
497     case 'number':
498       return src
499       break
500     case 'function':
501       obj = eval(src.toSource())
502       return Object.extend(obj, src)
503       break
504     case 'object':
505       if (src instanceof Array) {
506         return Object.extend([], src)
507       } else {
508         return Object.extend({}, src)
509       }
510       break
511   }
512 }
513 
514 /**
515   Creates and returns an Image object, with source URL set to src and
516   onload handler set to onload.
517 
518   @param {String} src The source URL for the image
519   @param {Function} onload The onload handler for the image
520   @return The created Image object
521   @type {Image}
522   */
523 Object.loadImage = function(src, onload) {
524   var img = new Image()
525   if (onload)
526     img.onload = onload
527   img.src = src
528   return img
529 }
530 
531 /**
532   Returns true if image is fully loaded and ready for use.
533 
534   @param image The image to check
535   @return Whether the image is loaded or not
536   @type {boolean}
537   @addon
538   */
539 Object.isImageLoaded = function(image) {
540   if (image.tagName == 'CANVAS') return true
541   if (!image.complete) return false
542   if (image.naturalWidth == null) return true
543   return !!image.naturalWidth
544 }
545 
546 /**
547   Sums two objects.
548   */
549 Object.sum = function(a,b) {
550   if (a instanceof Array) {
551     if (b instanceof Array) {
552       var ab = []
553       for (var i=0; i<a.length; i++) {
554         ab[i] = a[i] + b[i]
555       }
556       return ab
557     } else {
558       return a.map(function(v){ return v + b })
559     }
560   } else if (b instanceof Array) {
561     return b.map(function(v){ return v + a })
562   } else {
563     return a + b
564   }
565 }
566 
567 /**
568   Substracts b from a.
569   */
570 Object.sub = function(a,b) {
571   if (a instanceof Array) {
572     if (b instanceof Array) {
573       var ab = []
574       for (var i=0; i<a.length; i++) {
575         ab[i] = a[i] - b[i]
576       }
577       return ab
578     } else {
579       return a.map(function(v){ return v - b })
580     }
581   } else if (b instanceof Array) {
582     return b.map(function(v){ return a - v })
583   } else {
584     return a - b
585   }
586 }
587 
588 if (!window.Mouse) Mouse = {}
589 /**
590   Returns the coordinates for a mouse event relative to element.
591   Element must be the target for the event.
592 
593   @param element The element to compare against
594   @param event The mouse event
595   @return An object of form {x: relative_x, y: relative_y}
596   */
597 Mouse.getRelativeCoords = function(element, event) {
598   var xy = {x:0, y:0}
599   var osl = 0
600   var ost = 0
601   var el = element
602   while (el) {
603     osl += el.offsetLeft
604     ost += el.offsetTop
605     el = el.offsetParent
606   }
607   xy.x = event.pageX - osl
608   xy.y = event.pageY - ost
609   return xy
610 }
611 
612 Browser = (function(){
613   var ua = window.navigator.userAgent
614   var khtml = ua.match(/KHTML/)
615   var gecko = ua.match(/Gecko/)
616   var webkit = ua.match(/WebKit\/\d+/)
617   var ie = ua.match(/Explorer/)
618   if (khtml) return 'KHTML'
619   if (gecko) return 'Gecko'
620   if (webkit) return 'Webkit'
621   if (ie) return 'IE'
622   return 'UNKNOWN'
623 })()
624 
625 
626 Mouse.LEFT = 0
627 Mouse.MIDDLE = 1
628 Mouse.RIGHT = 2
629 
630 if (Browser == 'IE') {
631   Mouse.LEFT = 1
632   Mouse.MIDDLE = 4
633 }
634 
635 
636 /**
637   Klass is a function that returns a constructor function.
638 
639   The constructor function calls #initialize with its arguments.
640 
641   The parameters to Klass have their prototypes or themselves merged with the
642   constructor function's prototype.
643 
644   Finally, the constructor function's prototype is merged with the constructor
645   function. So you can write Shape.getArea.call(this) instead of
646   Shape.prototype.getArea.call(this).
647 
648   Shape = Klass({
649     getArea : function() {
650       raise('No area defined!')
651     }
652   })
653 
654   Rectangle = Klass(Shape, {
655     initialize : function(x, y) {
656       this.x = x
657       this.y = y
658     },
659 
660     getArea : function() {
661       return this.x * this.y
662     }
663   })
664 
665   Square = Klass(Rectangle, {
666     initialize : function(s) {
667       Rectangle.initialize.call(this, s, s)
668     }
669   })
670 
671   new Square(5).getArea()
672   //=> 25
673 
674   @return Constructor object for the class
675   */
676 Klass = function() {
677   var c = function() {
678     this.initialize.apply(this, arguments)
679   }
680   c.ancestors = $A(arguments)
681   c.prototype = {}
682   for(var i = 0; i<arguments.length; i++) {
683     var a = arguments[i]
684     if (a.prototype) {
685       Object.extend(c.prototype, a.prototype)
686     } else {
687       Object.extend(c.prototype, a)
688     }
689   }
690   Object.extend(c, c.prototype)
691   return c
692 }
693 
694 
695 
696 Curves = {
697 
698   angularDistance : function(a, b) {
699     var pi2 = Math.PI*2
700     var d = (b - a) % pi2
701     if (d > Math.PI) d -= pi2
702     if (d < -Math.PI) d += pi2
703     return d
704   },
705 
706   linePoint : function(a, b, t) {
707     return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t]
708   },
709 
710   quadraticPoint : function(a, b, c, t) {
711     // var d = this.linePoint(a,b,t)
712     // var e = this.linePoint(b,c,t)
713     // return this.linePoint(d,e,t)
714     var dx = a[0]+(b[0]-a[0])*t
715     var ex = b[0]+(c[0]-b[0])*t
716     var x = dx+(ex-dx)*t
717     var dy = a[1]+(b[1]-a[1])*t
718     var ey = b[1]+(c[1]-b[1])*t
719     var y = dy+(ey-dy)*t
720     return [x,y]
721   },
722 
723   cubicPoint : function(a, b, c, d, t) {
724     var ax3 = a[0]*3
725     var bx3 = b[0]*3
726     var cx3 = c[0]*3
727     var ay3 = a[1]*3
728     var by3 = b[1]*3
729     var cy3 = c[1]*3
730     return [
731       a[0] + t*(bx3 - ax3 + t*(ax3-2*bx3+cx3 + t*(bx3-a[0]-cx3+d[0]))),
732       a[1] + t*(by3 - ay3 + t*(ay3-2*by3+cy3 + t*(by3-a[1]-cy3+d[1])))
733     ]
734   },
735 
736   linearValue : function(a,b,t) {
737     return a + (b-a)*t
738   },
739 
740   quadraticValue : function(a,b,c,t) {
741     var d = a + (b-a)*t
742     var e = b + (c-b)*t
743     return d + (e-d)*t
744   },
745 
746   cubicValue : function(a,b,c,d,t) {
747     var a3 = a*3, b3 = b*3, c3 = c*3
748     return a + t*(b3 - a3 + t*(a3-2*b3+c3 + t*(b3-a-c3+d)))
749   },
750 
751   catmullRomPoint : function (a,b,c,d, t) {
752     var af = ((-t+2)*t-1)*t*0.5
753     var bf = (((3*t-5)*t)*t+2)*0.5
754     var cf = ((-3*t+4)*t+1)*t*0.5
755     var df = ((t-1)*t*t)*0.5
756     return [
757       a[0]*af + b[0]*bf + c[0]*cf + d[0]*df,
758       a[1]*af + b[1]*bf + c[1]*cf + d[1]*df
759     ]
760   },
761 
762   catmullRomAngle : function (a,b,c,d, t) {
763     var dx = 0.5 * (c[0] - a[0] + 2*t*(2*a[0] - 5*b[0] + 4*c[0] - d[0]) +
764              3*t*t*(3*b[0] + d[0] - a[0] - 3*c[0]))
765     var dy = 0.5 * (c[1] - a[1] + 2*t*(2*a[1] - 5*b[1] + 4*c[1] - d[1]) +
766              3*t*t*(3*b[1] + d[1] - a[1] - 3*c[1]))
767     return Math.atan2(dy, dx)
768   },
769 
770   catmullRomPointAngle : function (a,b,c,d, t) {
771     var p = this.catmullRomPoint(a,b,c,d,t)
772     var a = this.catmullRomAngle(a,b,c,d,t)
773     return {point:p, angle:a}
774   },
775 
776   lineAngle : function(a,b) {
777     return Math.atan2(b[1]-a[1], b[0]-a[0])
778   },
779 
780   quadraticAngle : function(a,b,c,t) {
781     var d = this.linePoint(a,b,t)
782     var e = this.linePoint(b,c,t)
783     return this.lineAngle(d,e)
784   },
785 
786   cubicAngle : function(a, b, c, d, t) {
787     var e = this.quadraticPoint(a,b,c,t)
788     var f = this.quadraticPoint(b,c,d,t)
789     return this.lineAngle(e,f)
790   },
791 
792   lineLength : function(a,b) {
793     var x = (b[0]-a[0])
794     var y = (b[1]-a[1])
795     return Math.sqrt(x*x + y*y)
796   },
797 
798   squareLineLength : function(a,b) {
799     var x = (b[0]-a[0])
800     var y = (b[1]-a[1])
801     return x*x + y*y
802   },
803 
804   quadraticLength : function(a,b,c, error) {
805     var p1 = this.linePoint(a,b,2/3)
806     var p2 = this.linePoint(b,c,1/3)
807     return this.cubicLength(a,p1,p2,c, error)
808   },
809 
810   cubicLength : (function() {
811     var bezsplit = function(v) {
812       var vtemp = [v.slice(0)]
813 
814       for (var i=1; i < 4; i++) {
815         vtemp[i] = [[],[],[],[]]
816         for (var j=0; j < 4-i; j++) {
817           vtemp[i][j][0] = 0.5 * (vtemp[i-1][j][0] + vtemp[i-1][j+1][0])
818           vtemp[i][j][1] = 0.5 * (vtemp[i-1][j][1] + vtemp[i-1][j+1][1])
819         }
820       }
821       var left = []
822       var right = []
823       for (var j=0; j<4; j++) {
824         left[j] = vtemp[j][0]
825         right[j] = vtemp[3-j][j]
826       }
827       return [left, right]
828     }
829 
830     var addifclose = function(v, error) {
831       var len = 0
832       for (var i=0; i < 3; i++) {
833         len += Curves.lineLength(v[i], v[i+1])
834       }
835       var chord = Curves.lineLength(v[0], v[3])
836       if ((len - chord) > error) {
837         var lr = bezsplit(v)
838         len = addifclose(lr[0], error) + addifclose(lr[1], error)
839       }
840       return len
841     }
842 
843     return function(a,b,c,d, error) {
844       if (!error) error = 1
845       return addifclose([a,b,c,d], error)
846     }
847   })(),
848 
849   quadraticLengthPointAngle : function(a,b,c,lt,error) {
850     var p1 = this.linePoint(a,b,2/3)
851     var p2 = this.linePoint(b,c,1/3)
852     return this.cubicLengthPointAngle(a,p1,p2,c, error)
853   },
854 
855   cubicLengthPointAngle : function(a,b,c,d,lt,error) {
856     // this thing outright rapes the GC.
857     // how about not creating a billion arrays, hmm?
858     var len = this.cubicLength(a,b,c,d,error)
859     var point = a
860     var prevpoint = a
861     var lengths = []
862     var prevlensum = 0
863     var lensum = 0
864     var tl = lt*len
865     var segs = 20
866     var fac = 1/segs
867     for (var i=1; i<=segs; i++) { // FIXME get smarter
868       prevpoint = point
869       point = this.cubicPoint(a,b,c,d, fac*i)
870       prevlensum = lensum
871       lensum += this.lineLength(prevpoint, point)
872       if (lensum >= tl) {
873         if (lensum == prevlensum)
874           return {point: point, angle: this.lineAngle(a,b)}
875         var dl = lensum - tl
876         var dt = dl / (lensum-prevlensum)
877         return {point: this.linePoint(prevpoint, point, 1-dt),
878                 angle: this.cubicAngle(a,b,c,d, fac*(i-dt)) }
879       }
880     }
881     return {point: d.slice(0), angle: this.lineAngle(c,d)}
882   }
883 
884 }
885 
886 
887 
888 /**
889   Color helper functions.
890   */
891 Colors = {
892 
893   /**
894     Converts an HSL color to its corresponding RGB color.
895 
896     @param h Hue in degrees (0 .. 359)
897     @param s Saturation (0.0 .. 1.0)
898     @param l Lightness (0 .. 255)
899     @return The corresponding RGB color as [r,g,b]
900     @type Array
901     */
902   hsl2rgb : function(h,s,l) {
903     var r,g,b
904     if (s == 0) {
905       r=g=b=v
906     } else {
907       var q = (l < 0.5 ? l * (1+s) : l+s-(l*s))
908       var p = 2 * l - q
909       var hk = (h % 360) / 360
910       var tr = hk + 1/3
911       var tg = hk
912       var tb = hk - 1/3
913       if (tr < 0) tr++
914       if (tr > 1) tr--
915       if (tg < 0) tg++
916       if (tg > 1) tg--
917       if (tb < 0) tb++
918       if (tb > 1) tb--
919       if (tr < 1/6)
920         r = p + ((q-p)*6*tr)
921       else if (tr < 1/2)
922         r = q
923       else if (tr < 2/3)
924         r = p + ((q-p)*6*(2/3 - tr))
925       else
926         r = p
927 
928       if (tg < 1/6)
929         g = p + ((q-p)*6*tg)
930       else if (tg < 1/2)
931         g = q
932       else if (tg < 2/3)
933         g = p + ((q-p)*6*(2/3 - tg))
934       else
935         g = p
936 
937       if (tb < 1/6)
938         b = p + ((q-p)*6*tb)
939       else if (tb < 1/2)
940         b = q
941       else if (tb < 2/3)
942         b = p + ((q-p)*6*(2/3 - tb))
943       else
944         b = p
945     }
946 
947     return [r,g,b]
948   },
949 
950   /**
951     Converts an HSV color to its corresponding RGB color.
952 
953     @param h Hue in degrees (0 .. 359)
954     @param s Saturation (0.0 .. 1.0)
955     @param v Value (0 .. 255)
956     @return The corresponding RGB color as [r,g,b]
957     @type Array
958     */
959   hsv2rgb : function(h,s,v) {
960     var r,g,b
961     if (s == 0) {
962       r=g=b=v
963     } else {
964       h = (h % 360)/60.0
965       var i = Math.floor(h)
966       var f = h-i
967       var p = v * (1-s)
968       var q = v * (1-s*f)
969       var t = v * (1-s*(1-f))
970       switch (i) {
971         case 0:
972           r = v
973           g = t
974           b = p
975           break
976         case 1:
977           r = q
978           g = v
979           b = p
980           break
981         case 2:
982           r = p
983           g = v
984           b = t
985           break
986         case 3:
987           r = p
988           g = q
989           b = v
990           break
991         case 4:
992           r = t
993           g = p
994           b = v
995           break
996         case 5:
997           r = v
998           g = p
999           b = q
1000           break
1001       }
1002     }
1003     return [r,g,b]
1004   },
1005 
1006   /**
1007     Parses a color style object into one that can be used with the given
1008     canvas context.
1009 
1010     Accepted formats:
1011       'white'
1012       '#fff'
1013       '#ffffff'
1014       'rgba(255,255,255, 1.0)'
1015       [255, 255, 255]
1016       [255, 255, 255, 1.0]
1017       new Gradient(...)
1018       new Pattern(...)
1019 
1020     @param style The color style to parse
1021     @param ctx Canvas 2D context on which the style is to be used
1022     @return A parsed style, ready to be used as ctx.fillStyle / strokeStyle
1023     */
1024   parseColorStyle : function(style, ctx) {
1025     if (typeof style == 'string') {
1026       return style
1027     } else if (style.compiled) {
1028       return style.compiled
1029     } else if (style.isPattern) {
1030       return style.compile(ctx)
1031     } else if (style.length == 3) {
1032       return 'rgba('+style.map(Math.round).join(",")+', 1)'
1033     } else if (style.length == 4) {
1034       return 'rgba('+
1035               Math.round(style[0])+','+
1036               Math.round(style[1])+','+
1037               Math.round(style[2])+','+
1038               style[3]+
1039              ')'
1040     } else { // wtf
1041       throw( "Bad style: " + style )
1042     }
1043   }
1044 }
1045 
1046 
1047 
1048 /**
1049   Navigating around differing implementations of canvas features.
1050 
1051   Current issues:
1052 
1053   isPointInPath(x,y):
1054 
1055     Opera supports isPointInPath.
1056 
1057     Safari doesn't have isPointInPath. So you need to keep track of the CTM and
1058     do your own in-fill-checking. Which is done for circles and rectangles
1059     in Circle#isPointInPath and Rectangle#isPointInPath.
1060     Paths use an inaccurate bounding box test, implemented in
1061     Path#isPointInPath.
1062 
1063     Firefox 3 has isPointInPath. But it uses user-space coordinates.
1064     Which can be easily navigated around because it has setTransform.
1065 
1066     Firefox 2 has isPointInPath. But it uses user-space coordinates.
1067     And there's no setTransform, so you need to keep track of the CTM and
1068     multiply the mouse vector with the CTM's inverse.
1069 
1070   Drawing text:
1071 
1072     Rhino has ctx.drawString(x,y, text)
1073 
1074     Firefox has ctx.mozDrawText(text)
1075 
1076     The WhatWG spec, Safari and Opera have nothing.
1077 
1078 */
1079 CanvasSupport = {
1080   DEVICE_SPACE : 0, // Opera
1081   USER_SPACE : 1,   // Fx2, Fx3
1082   isPointInPathMode : null,
1083   supportsIsPointInPath : null,
1084 
1085   getSupportsCSSTransform : function() {
1086     if (this.supportsCSSTransform == null) {
1087       var s = false
1088       var dbs = document.body.style
1089       s = (dbs.webkitTransform || dbs.MozTransform)
1090       this.supportsCSSTransform = s
1091     }
1092     return this.supportsWebKitTransform
1093   },
1094 
1095   getTestContext : function() {
1096     if (!this.testContext) {
1097       var c = E.canvas(1,1)
1098       this.testContext = c.getContext('2d')
1099     }
1100     return this.testContext
1101   },
1102 
1103   getSupportsAudioTag : function() {
1104     var e = E('audio')
1105     return !!e.play
1106   },
1107 
1108   getSupportsSoundManager : function() {
1109     return (window.soundManager && soundManager.enabled)
1110   },
1111 
1112   soundId : 0,
1113 
1114   getSoundObject : function() {
1115     var e = null
1116     if (this.getSupportsAudioTag()) {
1117       e = this.getAudioTagSoundObject()
1118     } else if (this.getSupportsSoundManager()) {
1119       e = this.getSoundManagerSoundObject()
1120     }
1121     return e
1122   },
1123 
1124   getAudioTagSoundObject : function() {
1125     var sid = 'sound-' + this.soundId++
1126     var e = E('audio', {id: sid})
1127     e.load = function(src) {
1128       this.src = src
1129     }
1130     e.addEventListener('canplaythrough', function() {
1131       if (this.onready) this.onready()
1132     }, false)
1133     e.setVolume = function(v){ this.volume = v }
1134     e.setPan = function(v){ this.pan = v }
1135     return e
1136   },
1137 
1138   getSoundManagerSoundObject : function() {
1139     var sid = 'sound-' + this.soundId++
1140     var e = {
1141       volume: 100,
1142       pan: 0,
1143       sid : sid,
1144       load : function(src) {
1145         return soundManager.load(this.sid, {
1146           url: src,
1147           autoPlay: false,
1148           volume: this.volume,
1149           pan: this.pan
1150         })
1151       },
1152       _onload : function() {
1153         if (this.onload) this.onload()
1154         if (this.onready) this.onready()
1155       },
1156       _onerror : function() {
1157         if (this.onerror) this.onerror()
1158       },
1159       _onfinish : function() {
1160         if (this.onfinish) this.onfinish()
1161       },
1162       play : function() {
1163         return soundManager.play(this.sid)
1164       },
1165       stop : function() {
1166         return soundManager.stop(this.sid)
1167       },
1168       pause : function() {
1169         return soundManager.togglePause(this.sid)
1170       },
1171       setVolume : function(v) {
1172         this.volume = v*100
1173         return soundManager.setVolume(this.sid, v*100)
1174       },
1175       setPan : function(v) {
1176         this.pan = v*100
1177         return soundManager.setPan(this.sid, v*100)
1178       }
1179     }
1180     soundManager.createSound(sid, 'null.mp3')
1181     e.sound = soundManager.getSoundById(sid)
1182     e.sound.options.onfinish = e._onfinish.bind(e)
1183     e.sound.options.onload = e._onload.bind(e)
1184     e.sound.options.onerror = e._onerror.bind(e)
1185     return e
1186   },
1187 
1188   /**
1189     Canvas context augment module that adds setters.
1190     */
1191   ContextSetterAugment : {
1192     setFillStyle : function(fs) { this.fillStyle = fs },
1193     setStrokeStyle : function(ss) { this.strokeStyle = ss },
1194     setGlobalAlpha : function(ga) { this.globalAlpha = ga },
1195     setLineWidth : function(lw) { this.lineWidth = lw },
1196     setLineCap : function(lw) { this.lineCap = lw },
1197     setLineJoin : function(lw) { this.lineJoin = lw },
1198     setMiterLimit : function(lw) { this.miterLimit = lw },
1199     setGlobalCompositeOperation : function(lw) {
1200       this.globalCompositeOperation = lw
1201     },
1202     setShadowColor : function(x) { this.shadowColor = x },
1203     setShadowBlur : function(x) { this.shadowBlur = x },
1204     setShadowOffsetX : function(x) { this.shadowOffsetX = x },
1205     setShadowOffsetY : function(x) { this.shadowOffsetY = x },
1206     setMozTextStyle : function(x) { this.mozTextStyle = x }
1207   },
1208 
1209   ContextJSImplAugment : {
1210     identity : function() {
1211       CanvasSupport.setTransform(this, [1,0,0,1,0,0])
1212     }
1213   },
1214 
1215   /**
1216     Augments a canvas context with setters.
1217     */
1218   augment : function(ctx) {
1219     Object.conditionalExtend(ctx, this.ContextSetterAugment)
1220     Object.conditionalExtend(ctx, this.ContextJSImplAugment)
1221     return ctx
1222   },
1223 
1224   /**
1225     Gets the augmented context for canvas.
1226     */
1227   getContext : function(canvas, type) {
1228     var ctx = canvas.getContext(type || '2d')
1229     this.augment(ctx)
1230     return ctx
1231   },
1232 
1233 
1234   /**
1235     Multiplies two 3x2 affine 2D column-major transformation matrices with
1236     each other and stores the result in the first matrix.
1237 
1238     Returns the multiplied matrix m1.
1239     */
1240   tMatrixMultiply : function(m1, m2) {
1241     var m11 = m1[0]*m2[0] + m1[2]*m2[1]
1242     var m12 = m1[1]*m2[0] + m1[3]*m2[1]
1243 
1244     var m21 = m1[0]*m2[2] + m1[2]*m2[3]
1245     var m22 = m1[1]*m2[2] + m1[3]*m2[3]
1246 
1247     var dx = m1[0]*m2[4] + m1[2]*m2[5] + m1[4]
1248     var dy = m1[1]*m2[4] + m1[3]*m2[5] + m1[5]
1249 
1250     m1[0] = m11
1251     m1[1] = m12
1252     m1[2] = m21
1253     m1[3] = m22
1254     m1[4] = dx
1255     m1[5] = dy
1256 
1257     return m1
1258   },
1259 
1260   /**
1261     Multiplies the vector [x, y, 1] with the 3x2 transformation matrix m.
1262     */
1263   tMatrixMultiplyPoint : function(m, x, y) {
1264     return [
1265       x*m[0] + y*m[2] + m[4],
1266       x*m[1] + y*m[3] + m[5]
1267     ]
1268   },
1269 
1270   /**
1271     Inverts a 3x2 affine 2D column-major transformation matrix.
1272 
1273     Returns an inverted copy of the matrix.
1274     */
1275   tInvertMatrix : function(m) {
1276     var d = 1 / (m[0]*m[3]-m[1]*m[2])
1277     return [
1278       m[3]*d, -m[1]*d,
1279       -m[2]*d, m[0]*d,
1280       d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5])
1281     ]
1282   },
1283 
1284   /**
1285     Applies a transformation matrix m on the canvas context ctx.
1286     */
1287   transform : function(ctx, m) {
1288     if (ctx.transform)
1289       return ctx.transform.apply(ctx, m)
1290     ctx.translate(m[4], m[5])
1291     // scale
1292     if (Math.abs(m[1]) < 1e-6 && Math.abs(m[2]) < 1e-6) {
1293       ctx.scale(m[0], m[3])
1294       return
1295     }
1296     var res = this.svdTransform({xx:m[0], xy:m[2], yx:m[1], yy:m[3], dx:m[4], dy:m[5]})
1297     ctx.rotate(res.angle2)
1298     ctx.scale(res.sx, res.sy)
1299     ctx.rotate(res.angle1)
1300     return
1301   },
1302 
1303   // broken svd...
1304   brokenSvd : function(m) {
1305     var mt = [m[0], m[2], m[1], m[3], 0,0]
1306     var mtm = [
1307       mt[0]*m[0]+mt[2]*m[1],
1308       mt[1]*m[0]+mt[3]*m[1],
1309       mt[0]*m[2]+mt[2]*m[3],
1310       mt[1]*m[2]+mt[3]*m[3],
1311       0,0
1312     ]
1313     // (mtm[0]-x) * (mtm[3]-x) - (mtm[1]*mtm[2]) = 0
1314     // x*x - (mtm[0]+mtm[3])*x - (mtm[1]*mtm[2])+(mtm[0]*mtm[3]) = 0
1315     var a = 1
1316     var b = -(mtm[0]+mtm[3])
1317     var c = -(mtm[1]*mtm[2])+(mtm[0]*mtm[3])
1318     var d = Math.sqrt(b*b - 4*a*c)
1319     var c1 = (-b + d) / (2*a)
1320     var c2 = (-b - d) / (2*a)
1321     if (c1 < c2)
1322       var tmp = c1, c1 = c2, c2 = tmp
1323     var s1 = Math.sqrt(c1)
1324     var s2 = Math.sqrt(c2)
1325     var i_s = [1/s1, 0, 0, 1/s2, 0,0]
1326     // (mtm[0]-c1)*x1 + mtm[2]*x2 = 0
1327     // mtm[1]*x1 + (mtm[3]-c1)*x2 = 0
1328     // x2 = -(mtm[0]-c1)*x1 / mtm[2]
1329     var e = ((mtm[0]-c1)/mtm[2])
1330     var l = Math.sqrt(1 + e*e)
1331     var v00 = 1 / l
1332     var v10 = e / l
1333     var v11 = v00
1334     var v01 = -v10
1335     var v = [v00, v01, v10, v11, 0,0]
1336     var u = m.slice(0)
1337     this.tMatrixMultiply(u,v)
1338     this.tMatrixMultiply(u,i_s)
1339     return [u, [s1,0,0,s2,0,0], [v00, v10, v01, v11, 0, 0]]
1340   },
1341 
1342 
1343   svdTransform : (function(){
1344     //   Copyright (c) 2004-2005, The Dojo Foundation
1345     //   All Rights Reserved
1346     var m = {}
1347     m.Matrix2D = function(arg){
1348       // summary: a 2D matrix object
1349       // description: Normalizes a 2D matrix-like object. If arrays is passed,
1350       //    all objects of the array are normalized and multiplied sequentially.
1351       // arg: Object
1352       //    a 2D matrix-like object, a number, or an array of such objects
1353       if(arg){
1354         if(typeof arg == "number"){
1355           this.xx = this.yy = arg;
1356         }else if(arg instanceof Array){
1357           if(arg.length > 0){
1358             var matrix = m.normalize(arg[0]);
1359             // combine matrices
1360             for(var i = 1; i < arg.length; ++i){
1361               var l = matrix, r = m.normalize(arg[i]);
1362               matrix = new m.Matrix2D();
1363               matrix.xx = l.xx * r.xx + l.xy * r.yx;
1364               matrix.xy = l.xx * r.xy + l.xy * r.yy;
1365               matrix.yx = l.yx * r.xx + l.yy * r.yx;
1366               matrix.yy = l.yx * r.xy + l.yy * r.yy;
1367               matrix.dx = l.xx * r.dx + l.xy * r.dy + l.dx;
1368               matrix.dy = l.yx * r.dx + l.yy * r.dy + l.dy;
1369             }
1370             Object.extend(this, matrix);
1371           }
1372         }else{
1373           Object.extend(this, arg);
1374         }
1375       }
1376     }
1377     // ensure matrix 2D conformance
1378     m.normalize = function(matrix){
1379         // summary: converts an object to a matrix, if necessary
1380         // description: Converts any 2D matrix-like object or an array of
1381         //    such objects to a valid dojox.gfx.matrix.Matrix2D object.
1382         // matrix: Object: an object, which is converted to a matrix, if necessary
1383         return (matrix instanceof m.Matrix2D) ? matrix : new m.Matrix2D(matrix); // dojox.gfx.matrix.Matrix2D
1384     }
1385     m.multiply = function(matrix){
1386       // summary: combines matrices by multiplying them sequentially in the given order
1387       // matrix: dojox.gfx.matrix.Matrix2D...: a 2D matrix-like object,
1388       //    all subsequent arguments are matrix-like objects too
1389       var M = m.normalize(matrix);
1390       // combine matrices
1391       for(var i = 1; i < arguments.length; ++i){
1392         var l = M, r = m.normalize(arguments[i]);
1393         M = new m.Matrix2D();
1394         M.xx = l.xx * r.xx + l.xy * r.yx;
1395         M.xy = l.xx * r.xy + l.xy * r.yy;
1396         M.yx = l.yx * r.xx + l.yy * r.yx;
1397         M.yy = l.yx * r.xy + l.yy * r.yy;
1398         M.dx = l.xx * r.dx + l.xy * r.dy + l.dx;
1399         M.dy = l.yx * r.dx + l.yy * r.dy + l.dy;
1400       }
1401       return M; // dojox.gfx.matrix.Matrix2D
1402     }
1403     m.invert = function(matrix) {
1404       var M = m.normalize(matrix),
1405         D = M.xx * M.yy - M.xy * M.yx,
1406         M = new m.Matrix2D({
1407           xx: M.yy/D, xy: -M.xy/D,
1408           yx: -M.yx/D, yy: M.xx/D,
1409           dx: (M.xy * M.dy - M.yy * M.dx) / D,
1410           dy: (M.yx * M.dx - M.xx * M.dy) / D
1411         });
1412       return M; // dojox.gfx.matrix.Matrix2D
1413     }
1414     // the default (identity) matrix, which is used to fill in missing values
1415     Object.extend(m.Matrix2D, {xx: 1, xy: 0, yx: 0, yy: 1, dx: 0, dy: 0});
1416 
1417     var eq = function(/* Number */ a, /* Number */ b){
1418       // summary: compare two FP numbers for equality
1419       return Math.abs(a - b) <= 1e-6 * (Math.abs(a) + Math.abs(b)); // Boolean
1420     };
1421 
1422     var calcFromValues = function(/* Number */ s1, /* Number */ s2){
1423       // summary: uses two close FP values to approximate the result
1424       if(!isFinite(s1)){
1425         return s2;  // Number
1426       }else if(!isFinite(s2)){
1427         return s1;  // Number
1428       }
1429       return (s1 + s2) / 2; // Number
1430     };
1431 
1432     var transpose = function(/* dojox.gfx.matrix.Matrix2D */ matrix){
1433       // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object
1434       var M = new m.Matrix2D(matrix);
1435       return Object.extend(M, {dx: 0, dy: 0, xy: M.yx, yx: M.xy}); // dojox.gfx.matrix.Matrix2D
1436     };
1437 
1438     var scaleSign = function(/* dojox.gfx.matrix.Matrix2D */ matrix){
1439       return (matrix.xx * matrix.yy < 0 || matrix.xy * matrix.yx > 0) ? -1 : 1; // Number
1440     };
1441 
1442     var eigenvalueDecomposition = function(/* dojox.gfx.matrix.Matrix2D */ matrix){
1443       // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object
1444       var M = m.normalize(matrix),
1445         b = -M.xx - M.yy,
1446         c = M.xx * M.yy - M.xy * M.yx,
1447         d = Math.sqrt(b * b - 4 * c),
1448         l1 = -(b + (b < 0 ? -d : d)) / 2,
1449         l2 = c / l1,
1450         vx1 = M.xy / (l1 - M.xx), vy1 = 1,
1451         vx2 = M.xy / (l2 - M.xx), vy2 = 1;
1452       if(eq(l1, l2)){
1453         vx1 = 1, vy1 = 0, vx2 = 0, vy2 = 1;
1454       }
1455       if(!isFinite(vx1)){
1456         vx1 = 1, vy1 = (l1 - M.xx) / M.xy;
1457         if(!isFinite(vy1)){
1458           vx1 = (l1 - M.yy) / M.yx, vy1 = 1;
1459           if(!isFinite(vx1)){
1460             vx1 = 1, vy1 = M.yx / (l1 - M.yy);
1461           }
1462         }
1463       }
1464       if(!isFinite(vx2)){
1465         vx2 = 1, vy2 = (l2 - M.xx) / M.xy;
1466         if(!isFinite(vy2)){
1467           vx2 = (l2 - M.yy) / M.yx, vy2 = 1;
1468           if(!isFinite(vx2)){
1469             vx2 = 1, vy2 = M.yx / (l2 - M.yy);
1470           }
1471         }
1472       }
1473       var d1 = Math.sqrt(vx1 * vx1 + vy1 * vy1),
1474         d2 = Math.sqrt(vx2 * vx2 + vy2 * vy2);
1475       if(isNaN(vx1 /= d1)){ vx1 = 0; }
1476       if(isNaN(vy1 /= d1)){ vy1 = 0; }
1477       if(isNaN(vx2 /= d2)){ vx2 = 0; }
1478       if(isNaN(vy2 /= d2)){ vy2 = 0; }
1479       return {  // Object
1480         value1: l1,
1481         value2: l2,
1482         vector1: {x: vx1, y: vy1},
1483         vector2: {x: vx2, y: vy2}
1484       };
1485     };
1486 
1487     var decomposeSR = function(/* dojox.gfx.matrix.Matrix2D */ M, /* Object */ result){
1488       // summary: decomposes a matrix into [scale, rotate]; no checks are done.
1489       var sign = scaleSign(M),
1490         a = result.angle1 = (Math.atan2(M.yx, M.yy) + Math.atan2(-sign * M.xy, sign * M.xx)) / 2,
1491         cos = Math.cos(a), sin = Math.sin(a);
1492       result.sx = calcFromValues(M.xx / cos, -M.xy / sin);
1493       result.sy = calcFromValues(M.yy / cos, M.yx / sin);
1494       return result;  // Object
1495     };
1496 
1497     var decomposeRS = function(/* dojox.gfx.matrix.Matrix2D */ M, /* Object */ result){
1498       // summary: decomposes a matrix into [rotate, scale]; no checks are done
1499       var sign = scaleSign(M),
1500         a = result.angle2 = (Math.atan2(sign * M.yx, sign * M.xx) + Math.atan2(-M.xy, M.yy)) / 2,
1501         cos = Math.cos(a), sin = Math.sin(a);
1502       result.sx = calcFromValues(M.xx / cos, M.yx / sin);
1503       result.sy = calcFromValues(M.yy / cos, -M.xy / sin);
1504       return result;  // Object
1505     };
1506 
1507     return function(matrix){
1508       // summary: decompose a 2D matrix into translation, scaling, and rotation components
1509       // description: this function decompose a matrix into four logical components:
1510       //  translation, rotation, scaling, and one more rotation using SVD.
1511       //  The components should be applied in following order:
1512       //  | [translate, rotate(angle2), scale, rotate(angle1)]
1513       // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object
1514       var M = m.normalize(matrix),
1515         result = {dx: M.dx, dy: M.dy, sx: 1, sy: 1, angle1: 0, angle2: 0};
1516       // detect case: [scale]
1517       if(eq(M.xy, 0) && eq(M.yx, 0)){
1518         return Object.extend(result, {sx: M.xx, sy: M.yy});  // Object
1519       }
1520       // detect case: [scale, rotate]
1521       if(eq(M.xx * M.yx, -M.xy * M.yy)){
1522         return decomposeSR(M, result);  // Object
1523       }
1524       // detect case: [rotate, scale]
1525       if(eq(M.xx * M.xy, -M.yx * M.yy)){
1526         return decomposeRS(M, result);  // Object
1527       }
1528       // do SVD
1529       var MT = transpose(M),
1530         u  = eigenvalueDecomposition([M, MT]),
1531         v  = eigenvalueDecomposition([MT, M]),
1532         U  = new m.Matrix2D({xx: u.vector1.x, xy: u.vector2.x, yx: u.vector1.y, yy: u.vector2.y}),
1533         VT = new m.Matrix2D({xx: v.vector1.x, xy: v.vector1.y, yx: v.vector2.x, yy: v.vector2.y}),
1534         S = new m.Matrix2D([m.invert(U), M, m.invert(VT)]);
1535       decomposeSR(VT, result);
1536       S.xx *= result.sx;
1537       S.yy *= result.sy;
1538       decomposeRS(U, result);
1539       S.xx *= result.sx;
1540       S.yy *= result.sy;
1541       return Object.extend(result, {sx: S.xx, sy: S.yy});  // Object
1542     };
1543   })(),
1544 
1545 
1546   /**
1547     Sets the canvas context ctx's transformation matrix to m, with ctm being
1548     the current transformation matrix.
1549     */
1550   setTransform : function(ctx, m, ctm) {
1551     if (ctx.setTransform)
1552       return ctx.setTransform.apply(ctx, m)
1553     this.transform(ctx, this.tInvertMatrix(ctm))
1554     this.transform(ctx, m)
1555   },
1556 
1557   /**
1558     Skews the canvas context by angle on the x-axis.
1559     */
1560   skewX : function(ctx, angle) {
1561     return this.transform(ctx, this.tSkewXMatrix(angle))
1562   },
1563 
1564   /**
1565     Skews the canvas context by angle on the y-axis.
1566     */
1567   skewY : function(ctx, angle) {
1568     return this.transform(ctx, this.tSkewYMatrix(angle))
1569   },
1570 
1571   /**
1572     Rotates a transformation matrix by angle.
1573     */
1574   tRotate : function(m1, angle) {
1575     // return this.tMatrixMultiply(matrix, this.tRotationMatrix(angle))
1576     var c = Math.cos(angle)
1577     var s = Math.sin(angle)
1578     var m11 = m1[0]*c + m1[2]*s
1579     var m12 = m1[1]*c + m1[3]*s
1580     var m21 = m1[0]*-s + m1[2]*c
1581     var m22 = m1[1]*-s + m1[3]*c
1582     m1[0] = m11
1583     m1[1] = m12
1584     m1[2] = m21
1585     m1[3] = m22
1586     return m1
1587   },
1588 
1589   /**
1590     Translates a transformation matrix by x and y.
1591     */
1592   tTranslate : function(m1, x, y) {
1593     // return this.tMatrixMultiply(matrix, this.tTranslationMatrix(x,y))
1594     m1[4] += m1[0]*x + m1[2]*y
1595     m1[5] += m1[1]*x + m1[3]*y
1596     return m1
1597   },
1598 
1599   /**
1600     Scales a transformation matrix by sx and sy.
1601     */
1602   tScale : function(m1, sx, sy) {
1603     // return this.tMatrixMultiply(matrix, this.tScalingMatrix(sx,sy))
1604     m1[0] *= sx
1605     m1[1] *= sx
1606     m1[2] *= sy
1607     m1[3] *= sy
1608     return m1
1609   },
1610 
1611   /**
1612     Skews a transformation matrix by angle on the x-axis.
1613     */
1614   tSkewX : function(m1, angle) {
1615     return this.tMatrixMultiply(m1, this.tSkewXMatrix(angle))
1616   },
1617 
1618   /**
1619     Skews a transformation matrix by angle on the y-axis.
1620     */
1621   tSkewY : function(m1, angle) {
1622     return this.tMatrixMultiply(m1, this.tSkewYMatrix(angle))
1623   },
1624 
1625   /**
1626     Returns a 3x2 2D column-major y-skew matrix for the angle.
1627     */
1628   tSkewXMatrix : function(angle) {
1629     return [ 1, 0, Math.tan(angle), 1, 0, 0 ]
1630   },
1631 
1632   /**
1633     Returns a 3x2 2D column-major y-skew matrix for the angle.
1634     */
1635   tSkewYMatrix : function(angle) {
1636     return [ 1, Math.tan(angle), 0, 1, 0, 0 ]
1637   },
1638 
1639   /**
1640     Returns a 3x2 2D column-major rotation matrix for the angle.
1641     */
1642   tRotationMatrix : function(angle) {
1643     var c = Math.cos(angle)
1644     var s = Math.sin(angle)
1645     return [ c, s, -s, c, 0, 0 ]
1646   },
1647 
1648   /**
1649     Returns a 3x2 2D column-major translation matrix for x and y.
1650     */
1651   tTranslationMatrix : function(x, y) {
1652     return [ 1, 0, 0, 1, x, y ]
1653   },
1654 
1655   /**
1656     Returns a 3x2 2D column-major scaling matrix for sx and sy.
1657     */
1658   tScalingMatrix : function(sx, sy) {
1659     return [ sx, 0, 0, sy, 0, 0 ]
1660   },
1661 
1662   /**
1663     Returns the name of the text backend to use.
1664 
1665     Possible values are:
1666       * 'MozText' for Firefox
1667       * 'DrawString' for Rhino
1668       * 'NONE' no text drawing
1669 
1670     @return The text backend name
1671     @type String
1672     */
1673   getTextBackend : function() {
1674     if (this.textBackend == null)
1675       this.textBackend = this.detectTextBackend()
1676     return this.textBackend
1677   },
1678 
1679   /**
1680     Detects the name of the text backend to use.
1681 
1682     Possible values are:
1683       * 'MozText' for Firefox
1684       * 'DrawString' for Rhino
1685       * 'NONE' no text drawing
1686 
1687     @return The text backend name
1688     @type String
1689     */
1690   detectTextBackend : function() {
1691     var ctx = this.getTestContext()
1692     if (ctx.mozDrawText) {
1693       return 'MozText'
1694     } else if (ctx.drawString) {
1695       return 'DrawString'
1696     }
1697     return 'NONE'
1698   },
1699 
1700   getSupportsPutImageData : function() {
1701     if (this.supportsPutImageData == null) {
1702       var ctx = this.getTestContext()
1703       var support = ctx.putImageData
1704       if (support) {
1705         try {
1706           var idata = ctx.getImageData(0,0,1,1)
1707           idata[0] = 255
1708           idata[1] = 0
1709           idata[2] = 255
1710           idata[3] = 255
1711           ctx.putImageData({width: 1, height: 1, data: idata}, 0, 0)
1712           var idata = ctx.getImageData(0,0,1,1)
1713           support = [255, 0, 255, 255].equals(idata.data)
1714         } catch(e) {
1715           support = false
1716         }
1717       }
1718       this.supportsPutImageData = support
1719     }
1720     return support
1721   },
1722 
1723   /**
1724     Returns true if the browser can be coaxed to work with
1725     {@link CanvasSupport.isPointInPath}.
1726 
1727     @return Whether the browser supports isPointInPath or not
1728     @type boolean
1729     */
1730   getSupportsIsPointInPath : function() {
1731     if (this.supportsIsPointInPath == null)
1732       this.supportsIsPointInPath = !!this.getTestContext().isPointInPath
1733     return this.supportsIsPointInPath
1734   },
1735 
1736   /**
1737     Returns the coordinate system in which the isPointInPath of the
1738     browser operates. Possible coordinate systems are
1739     CanvasSupport.DEVICE_SPACE and CanvasSupport.USER_SPACE.
1740 
1741     @return The coordinate system for the browser's isPointInPath
1742     */
1743   getIsPointInPathMode : function() {
1744     if (this.isPointInPathMode == null)
1745       this.isPointInPathMode = this.detectIsPointInPathMode()
1746     return this.isPointInPathMode
1747   },
1748 
1749   /**
1750     Detects the coordinate system in which the isPointInPath of the
1751     browser operates. Possible coordinate systems are
1752     CanvasSupport.DEVICE_SPACE and CanvasSupport.USER_SPACE.
1753 
1754     @return The coordinate system for the browser's isPointInPath
1755     @private
1756     */
1757   detectIsPointInPathMode : function() {
1758     var ctx = this.getTestContext()
1759     var rv
1760     if (!ctx.isPointInPath)
1761       return this.USER_SPACE
1762     ctx.save()
1763     ctx.translate(1,0)
1764     ctx.beginPath()
1765     ctx.rect(0,0,1,1)
1766     if (ctx.isPointInPath(0.3,0.3)) {
1767       rv = this.USER_SPACE
1768     } else {
1769       rv = this.DEVICE_SPACE
1770     }
1771     ctx.restore()
1772     return rv
1773   },
1774 
1775   /**
1776     Returns true if the device-space point (x,y) is inside the fill of
1777     ctx's current path.
1778 
1779     @param ctx Canvas 2D context to query
1780     @param x The distance in pixels from the left side of the canvas element
1781     @param y The distance in pixels from the top side of the canvas element
1782     @param matrix The current transformation matrix. Needed if the browser has
1783                   no isPointInPath or the browser's isPointInPath works in
1784                   user-space coordinates and the browser doesn't support
1785                   setTransform.
1786     @param callbackObj If the browser doesn't support isPointInPath,
1787                        callbackObj.isPointInPath will be called with the
1788                        x,y-coordinates transformed to user-space.
1789     @param
1790     @return Whether (x,y) is inside ctx's current path or not
1791     @type boolean
1792     */
1793   isPointInPath : function(ctx, x, y, matrix, callbackObj) {
1794     var rv
1795     if (!ctx.isPointInPath) {
1796       if (callbackObj && callbackObj.isPointInPath) {
1797         var xy = this.tMatrixMultiplyPoint(this.tInvertMatrix(matrix), x, y)
1798         return callbackObj.isPointInPath(xy[0], xy[1])
1799       } else {
1800         return false
1801       }
1802     } else {
1803       if (this.getIsPointInPathMode() == this.USER_SPACE) {
1804         if (!ctx.setTransform) {
1805           var xy = this.tMatrixMultiplyPoint(this.tInvertMatrix(matrix), x, y)
1806           rv = ctx.isPointInPath(xy[0], xy[1])
1807         } else {
1808           ctx.save()
1809           ctx.setTransform(1,0,0,1,0,0)
1810           rv = ctx.isPointInPath(x,y)
1811           ctx.restore()
1812         }
1813       } else {
1814         rv = ctx.isPointInPath(x,y)
1815       }
1816       return rv
1817     }
1818   }
1819 }
1820 
1821 
1822 RecordingContext = Klass({
1823   objectId : 0,
1824   commands : [],
1825   isMockObject : true,
1826 
1827   initialize : function(commands) {
1828     this.commands = commands || []
1829     Object.conditionalExtend(this, this.getMockContext())
1830   },
1831 
1832   getMockContext : function() {
1833     if (!RecordingContext.MockContext) {
1834       var c = E.canvas(1,1)
1835       var ctx = CanvasSupport.getContext(c, '2d')
1836       var obj = {}
1837       for (var i in ctx) {
1838         if (typeof(ctx[i]) == 'function')
1839           obj[i] = this.createRecordingFunction(i)
1840         else
1841           obj[i] = ctx[i]
1842       }
1843       obj.isPointInPath = null
1844       obj.transform = null
1845       obj.setTransform = null
1846       RecordingContext.MockContext = obj
1847     }
1848     return RecordingContext.MockContext
1849   },
1850 
1851   createRecordingFunction : function(name){
1852     if (name.search(/^set[A-Z]/) != -1 && name != 'setTransform') {
1853       var varName = name.charAt(3).toLowerCase() + name.slice(4)
1854       return function(){
1855         this[varName] = arguments[0]
1856         this.commands.push([name, $A(arguments)])
1857       }
1858     } else {
1859       return function(){
1860         this.commands.push([name, $A(arguments)])
1861       }
1862     }
1863   },
1864 
1865   clear : function(){
1866     this.commands = []
1867   },
1868 
1869   getRecording : function() {
1870     return this.commands
1871   },
1872 
1873   serialize : function(width, height) {
1874     return '(' + {
1875       width: width, height: height,
1876       commands: this.getRecording()
1877     }.toSource() + ')'
1878   },
1879 
1880   play : function(ctx) {
1881     RecordingContext.play(ctx, this.getRecording())
1882   },
1883 
1884   createLinearGradient : function() {
1885     var id = this.objectId++
1886     this.commands.push([id, '=', 'createLinearGradient', $A(arguments)])
1887     return new MockGradient(this, id)
1888   },
1889 
1890   createRadialGradient : function() {
1891     var id = this.objectId++
1892     this.commands.push([id, '=', 'createRadialGradient', $A(arguments)])
1893     return new this.MockGradient(this, id)
1894   },
1895 
1896   createPattern : function() {
1897     var id = this.objectId++
1898     this.commands.push([id, '=', 'createPattern', $A(arguments)])
1899     return new this.MockGradient(this, id)
1900   },
1901 
1902   MockGradient : Klass({
1903     isMockObject : true,
1904 
1905     initialize : function(recorder, id) {
1906       this.recorder = recorder
1907       this.id = id
1908     },
1909 
1910     addColorStop : function() {
1911       this.recorder.commands.push([this.id, 'addColorStop', $A(arguments)])
1912     },
1913 
1914     toSource : function() {
1915       return {id : this.id, isMockObject : true}.toSource()
1916     }
1917   })
1918 })
1919 RecordingContext.play = function(ctx, commands) {
1920   var dictionary = []
1921   for (var i=0; i<commands.length; i++) {
1922     var cmd = commands[i]
1923     if (cmd.length == 2) {
1924       var args = cmd[1]
1925       if (args[0] && args[0].isMockObject) {
1926         ctx[cmd[0]](dictionary[args[0].id])
1927       } else {
1928         ctx[cmd[0]].apply(ctx, cmd[1])
1929       }
1930     } else if (cmd.length == 3) {
1931       var obj = dictionary[cmd[0]]
1932       obj[cmd[1]].apply(obj, cmd[2])
1933     } else if (cmd.length == 4) {
1934       dictionary[cmd[0]] = ctx[cmd[2]].apply(ctx, cmd[3])
1935     } else {
1936       throw "Malformed command: "+cmd.toString()
1937     }
1938   }
1939 }
1940 
1941 
1942 
1943 
1944 Transformable = Klass({
1945   needMatrixUpdate : true,
1946 
1947   /**
1948     Transforms the context state according to this node's attributes.
1949 
1950     @param ctx Canvas 2D context
1951     */
1952   transform : function(ctx) {
1953     var atm = this.absoluteMatrix
1954     var xy = this.x || this.y
1955     var rot = this.rotation
1956     var sca = this.scale != null
1957     var skX = this.skewX
1958     var skY = this.skewY
1959     var tm = this.matrix
1960     var tl = this.transformList
1961 
1962     // update the node's transformation matrix
1963     if (this.needMatrixUpdate || !this.currentMatrix) {
1964       if (!this.currentMatrix) this.currentMatrix = [1,0,0,1,0,0]
1965       if (this.parent)
1966         this.__copyMatrix(this.parent.currentMatrix)
1967       else
1968         this.__identityMatrix()
1969       if (atm) this.__setMatrixMatrix(this.absoluteMatrix)
1970       if (xy) this.__translateMatrix(this.x, this.y)
1971       if (rot) this.__rotateMatrix(this.rotation)
1972       if (skX) this.__skewXMatrix(this.skewX)
1973       if (skY) this.__skewYMatrix(this.skewY)
1974       if (sca) this.__scaleMatrix(this.scale)
1975       if (tm) this.__matrixMatrix(this.matrix)
1976       if (tl) {
1977         for (var i=0; i<this.transformList.length; i++) {
1978           var tl = this.transformList[i]
1979           this['__'+tl[0]+'Matrix'](tl[1])
1980         }
1981       }
1982       this.needMatrixUpdate = false
1983     }
1984 
1985     if (!ctx) return
1986 
1987     // transform matrix modifiers
1988     if (atm) this.__setMatrix(ctx, this.absoluteMatrix)
1989     if (xy) this.__translate(ctx, this.x, this.y)
1990     if (rot) this.__rotate(ctx, this.rotation)
1991     if (skX) this.__skewX(ctx, this.skewX)
1992     if (skY) this.__skewY(ctx, this.skewY)
1993     if (sca) this.__scale(ctx, this.scale)
1994     if (tm) this.__matrix(ctx, this.matrix)
1995 
1996     if (tl) {
1997       for (var i=0; i<this.transformList.length; i++) {
1998         var tl = this.transformList[i]
1999         this['__'+tl[0]](ctx, tl[1])
2000       }
2001     }
2002   },
2003 
2004   distanceTo : function(node) {
2005     return Curves.lineLength([this.x, this.y], [node.x, node.y])
2006   },
2007 
2008   angleTo : function(node) {
2009     return Curves.lineAngle([this.x, this.y], [node.x, node.y])
2010   },
2011 
2012 
2013 
2014   __setMatrixMatrix : function(matrix) {
2015     if (!this.previousMatrix) this.previousMatrix = []
2016     var p = this.previousMatrix
2017     var c = this.currentMatrix
2018     p[0] = c[0]
2019     p[1] = c[1]
2020     p[2] = c[2]
2021     p[3] = c[3]
2022     p[4] = c[4]
2023     p[5] = c[5]
2024     p = this.currentMatrix
2025     c = matrix
2026     p[0] = c[0]
2027     p[1] = c[1]
2028     p[2] = c[2]
2029     p[3] = c[3]
2030     p[4] = c[4]
2031     p[5] = c[5]
2032   },
2033 
2034   __copyMatrix : function(matrix) {
2035     var p = this.currentMatrix
2036     var c = matrix
2037     p[0] = c[0]
2038     p[1] = c[1]
2039     p[2] = c[2]
2040     p[3] = c[3]
2041     p[4] = c[4]
2042     p[5] = c[5]
2043   },
2044 
2045   __identityMatrix : function() {
2046     var p = this.currentMatrix
2047     p[0] = 1
2048     p[1] = 0
2049     p[2] = 0
2050     p[3] = 1
2051     p[4] = 0
2052     p[5] = 0
2053   },
2054 
2055   __translateMatrix : function(x, y) {
2056     if (x.length) {
2057       CanvasSupport.tTranslate( this.currentMatrix, x[0], x[1] )
2058     } else {
2059       CanvasSupport.tTranslate( this.currentMatrix, x, y )
2060     }
2061   },
2062 
2063   __rotateMatrix : function(rotation) {
2064     if (rotation.length) {
2065       if (rotation[0] % Math.PI*2 == 0) return
2066       if (rotation[1] || rotation[2]) {
2067         CanvasSupport.tTranslate( this.currentMatrix,
2068                                   rotation[1], rotation[2] )
2069         CanvasSupport.tRotate( this.currentMatrix, rotation[0] )
2070         CanvasSupport.tTranslate( this.currentMatrix,
2071                                   -rotation[1], -rotation[2] )
2072       } else {
2073         CanvasSupport.tRotate( this.currentMatrix, rotation[0] )
2074       }
2075     } else {
2076       if (rotation % Math.PI*2 == 0) return
2077       CanvasSupport.tRotate( this.currentMatrix, rotation )
2078     }
2079   },
2080 
2081   __skewXMatrix : function(skewX) {
2082     if (skewX.length && skewX[0])
2083       CanvasSupport.tSkewX(this.currentMatrix, skewX[0])
2084     else
2085       CanvasSupport.tSkewX(this.currentMatrix, skewX)
2086   },
2087 
2088   __skewYMatrix : function(skewY) {
2089     if (skewY.length && skewY[0])
2090       CanvasSupport.tSkewY(this.currentMatrix, skewY[0])
2091     else
2092       CanvasSupport.tSkewY(this.currentMatrix, skewY)
2093   },
2094 
2095   __scaleMatrix : function(scale) {
2096     if (scale.length == 2) {
2097       if (scale[0] == 1 && scale[1] == 1) return
2098       CanvasSupport.tScale(this.currentMatrix,
2099                            scale[0], scale[1])
2100     } else if (scale.length == 3) {
2101       if (scale[0] == 1 || (scale[0].length && (scale[0][0] == 1 && scale[0][1] == 1)))
2102         return
2103       CanvasSupport.tTranslate(this.currentMatrix,
2104                                scale[1], scale[2])
2105       if (scale[0].length) {
2106         CanvasSupport.tScale(this.currentMatrix,
2107                               scale[0][0], scale[0][1])
2108       } else {
2109         CanvasSupport.tScale( this.currentMatrix, scale[0], scale[0] )
2110       }
2111       CanvasSupport.tTranslate(this.currentMatrix,
2112                                -scale[1], -scale[2])
2113     } else if (scale != 1) {
2114       CanvasSupport.tScale( this.currentMatrix, scale, scale )
2115     }
2116   },
2117 
2118   __matrixMatrix : function(matrix) {
2119     CanvasSupport.tMatrixMultiply(this.currentMatrix, matrix)
2120   },
2121 
2122   __setMatrix : function(ctx, matrix) {
2123     CanvasSupport.setTransform(ctx, matrix, this.previousMatrix)
2124   },
2125 
2126   __translate : function(ctx, x,y) {
2127     if (x.length != null)
2128       ctx.translate(x[0], x[1])
2129     else
2130       ctx.translate(x, y)
2131   },
2132 
2133   __rotate : function(ctx, rotation) {
2134     if (rotation.length) {
2135       if (rotation[1] || rotation[2]) {
2136         if (rotation[0] % Math.PI*2 == 0) return
2137         ctx.translate( rotation[1], rotation[2] )
2138         ctx.rotate( rotation[0] )
2139         ctx.translate( -rotation[1], -rotation[2] )
2140       } else {
2141         ctx.rotate( rotation[0] )
2142       }
2143     } else {
2144       ctx.rotate( rotation )
2145     }
2146   },
2147 
2148   __skewX : function(ctx, skewX) {
2149     if (skewX.length && skewX[0])
2150       CanvasSupport.skewX(ctx, skewX[0])
2151     else
2152       CanvasSupport.skewX(ctx, skewX)
2153   },
2154 
2155   __skewY : function(ctx, skewY) {
2156     if (skewY.length && skewY[0])
2157       CanvasSupport.skewY(ctx, skewY[0])
2158     else
2159       CanvasSupport.skewY(ctx, skewY)
2160   },
2161 
2162   __scale : function(ctx, scale) {
2163     if (scale.length == 2) {
2164       ctx.scale(scale[0], scale[1])
2165     } else if (scale.length == 3) {
2166       ctx.translate( scale[1], scale[2] )
2167       if (scale[0].length) {
2168         ctx.scale(scale[0][0], scale[0][1])
2169       } else {
2170         ctx.scale(scale[0], scale[0])
2171       }
2172       ctx.translate( -scale[1], -scale[2] )
2173     } else {
2174       ctx.scale(scale, scale)
2175     }
2176   },
2177 
2178   __matrix : function(ctx, matrix) {
2179     CanvasSupport.transform(ctx, matrix)
2180   }
2181 
2182 })
2183 
2184 
2185 /**
2186   Timeline is an animator that tweens between its frames.
2187 
2188   When object.time = k.time:
2189        object.state = k.state
2190   When object.time > k[i-1].time and object.time < k[i].time:
2191        object.state = k[i].tween(position, k[i-1].state, k[i].state)
2192        where position = elapsed / duration,
2193              elapsed = object.time - k[i-1].time,
2194              duration = k[i].time - k[i-1].time
2195   */
2196 Timeline = Klass({
2197   startTime : null,
2198   repeat : false,
2199 
2200   initialize : function(repeat, pingpong) {
2201     this.repeat = repeat
2202     this.keyframes = []
2203   },
2204 
2205   addKeyframe : function(time, target, tween) {
2206     if (arguments.length == 1) this.keyframes.push(time)
2207     else  this.keyframes.push({
2208             time : time,
2209             target : target,
2210             tween : tween
2211           })
2212   },
2213 
2214   evaluate : function(object, ot, dt) {
2215     if (this.startTime == null) this.startTime = ot
2216     var t = ot - this.startTime
2217     if (this.keyframes.length > 0) {
2218       // find current keyframe
2219       var currentIndex, previousFrame, currentFrame
2220       for (var i=0; i<this.keyframes.length; i++) {
2221         if (this.keyframes[i].time > t) {
2222           currentIndex = i
2223           break
2224         }
2225       }
2226       if (currentIndex != null) {
2227         previousFrame = this.keyframes[currentIndex-1]
2228         currentFrame = this.keyframes[currentIndex]
2229       }
2230       if (!currentFrame) {
2231         if (!this.keyframes.atEnd) {
2232           this.keyframes.atEnd = true
2233           previousFrame = this.keyframes[this.keyframes.length - 1]
2234           Object.extend(object, Object.clone(previousFrame.target))
2235           if (this.repeat) this.startTime = ot
2236           object.changed = true
2237         }
2238       } else if (previousFrame) {
2239         this.keyframes.atEnd = false
2240         // animate towards current keyframe
2241         var elapsed = t - previousFrame.time
2242         var duration = currentFrame.time - previousFrame.time
2243         var pos = elapsed / duration
2244         for (var k in currentFrame.target) {
2245           if (previousFrame.target[k] != null) {
2246             object.tweenVariable(k,
2247               previousFrame.target[k], currentFrame.target[k],
2248               pos, currentFrame.tween)
2249           }
2250         }
2251       }
2252     }
2253   }
2254 
2255 })
2256 
2257 
2258 Animatable = Klass({
2259   tweenFunctions : {
2260     linear : function(v) { return v },
2261 
2262     set : function(v) { return Math.floor(v) },
2263     discrete : function(v) { return Math.floor(v) },
2264 
2265     sine : function(v) { return 0.5-0.5*Math.cos(v*Math.PI) },
2266 
2267     sproing : function(v) {
2268       return (0.5-0.5*Math.cos(v*3.59261946538606)) * 1.05263157894737
2269       // pi + pi-acos(0.9)
2270     },
2271 
2272     square : function(v) {
2273       return v*v
2274     },
2275 
2276     cube : function(v) {
2277       return v*v*v
2278     },
2279 
2280     sqrt : function(v) {
2281       return Math.sqrt(v)
2282     },
2283 
2284     curt : function(v) {
2285       return Math.pow(v, -0.333333333333)
2286     }
2287   },
2288 
2289   initialize : function() {
2290     this.timeline = []
2291     this.keyframes = []
2292     this.pendingKeyframes = []
2293     this.pendingTimelineEvents = []
2294     this.timelines = []
2295     this.animators = []
2296     this.addFrameListener(this.updateTimelines)
2297     this.addFrameListener(this.updateKeyframes)
2298     this.addFrameListener(this.updateTimeline)
2299     this.addFrameListener(this.updateAnimators)
2300   },
2301 
2302   updateTimelines : function(t, dt) {
2303     for (var i=0; i<this.timelines.length; i++)
2304       this.timelines[i].evaluate(this, t, dt)
2305   },
2306 
2307   addTimeline : function(tl) {
2308     this.timelines.push(tl)
2309   },
2310 
2311   removeTimeline : function(tl) {
2312     this.timelines.deleteFirst(tl)
2313   },
2314 
2315   /**
2316     Tweens between keyframes (a keyframe is an object with the new values of
2317     the members of this, e.g. { time: 0, target: { x: 10, y: 20 }, tween: 'square'})
2318 
2319     Keyframes are very much like multi-variable animators, the main difference
2320     is that with keyframes the start value and the duration are implicit.
2321 
2322     While an animation from value A to B would take two keyframes instead of
2323     a single animator, chaining and reordering keyframes is very easy.
2324     */
2325   updateKeyframes : function(t,dt) {
2326     if (this.pendingKeyframes.length > 0) {
2327       while (this.pendingKeyframes.length > 0) {
2328         var kf = this.pendingKeyframes.pop()
2329         if (kf.time == null)
2330           kf.time = kf.relativeTime + t
2331         this.keyframes.push(kf)
2332       }
2333       this.keyframes.sort(function(a,b) { return a.time - b.time })
2334     }
2335     if (this.keyframes.length > 0) {
2336       // find current keyframe
2337       var currentIndex, previousFrame, currentFrame
2338       for (var i=0; i<this.keyframes.length; i++) {
2339         if (this.keyframes[i].time > t) {
2340           currentIndex = i
2341           break
2342         }
2343       }
2344       if (currentIndex != null) {
2345         previousFrame = this.keyframes[currentIndex-1]
2346         currentFrame = this.keyframes[currentIndex]
2347       }
2348       if (!currentFrame) {
2349         if (!this.keyframes.atEnd) {
2350           this.keyframes.atEnd = true
2351           previousFrame = this.keyframes[this.keyframes.length - 1]
2352           Object.extend(this, Object.clone(previousFrame.target))
2353           this.changed = true
2354         }
2355       } else if (previousFrame) {
2356         this.keyframes.atEnd = false
2357         // animate towards current keyframe
2358         var elapsed = t - previousFrame.time
2359         var duration = currentFrame.time - previousFrame.time
2360         var pos = elapsed / duration
2361         for (var k in currentFrame.target) {
2362           if (previousFrame.target[k] != null) {
2363             this.tweenVariable(k,
2364               previousFrame.target[k], currentFrame.target[k],
2365               pos, currentFrame.tween)
2366           }
2367         }
2368       }
2369     }
2370   },
2371 
2372   /**
2373     Run and remove timelineEvents that have startTime <= t.
2374     TimelineEvents are run in the ascending order of their startTimes.
2375     */
2376   updateTimeline : function(t, dt) {
2377     if (this.pendingTimelineEvents.length > 0) {
2378       while (this.pendingTimelineEvents.length > 0) {
2379         var kf = this.pendingTimelineEvents.pop()
2380         if (!kf.startTime)
2381           kf.startTime = kf.relativeStartTime + t
2382         this.timeline.push(kf)
2383       }
2384       this.timeline.sort(function(a,b) { return a.startTime - b.startTime })
2385     }
2386     while (this.timeline[0] && this.timeline[0].startTime <= t) {
2387       var keyframe = this.timeline.shift()
2388       var rv = true
2389       if (typeof(keyframe.action) == 'function')
2390         rv = keyframe.action.call(this, t, dt, keyframe)
2391       else
2392         this.animators.push(keyframe.action)
2393       if (keyframe.repeatEvery != null && rv != false) {
2394         if (keyframe.repeatTimes != null) {
2395           if (keyframe.repeatTimes <= 0) continue
2396           keyframe.repeatTimes--
2397         }
2398         keyframe.startTime += keyframe.repeatEvery
2399         this.addTimelineEvent(keyframe)
2400       }
2401       this.changed = true
2402     }
2403   },
2404 
2405   addTimelineEvent : function(kf) {
2406     this.pendingTimelineEvents.push(kf)
2407   },
2408 
2409   /**
2410     Run each animator, delete ones that have their durations exceeded.
2411     */
2412   updateAnimators : function(t, dt) {
2413     for (var i=0; i<this.animators.length; i++) {
2414       var ani = this.animators[i]
2415       if (!ani.startTime) ani.startTime = t
2416       var elapsed = t - ani.startTime
2417       var pos = elapsed / ani.duration
2418       var shouldRemove = false
2419       if (pos >= 1) {
2420         if (!ani.repeat) {
2421           pos = 1
2422           shouldRemove = true
2423         } else {
2424           if (ani.repeat !== true) ani.repeat = Math.max(0, ani.repeat - 1)
2425           if (ani.accumulate) {
2426             ani.startValue = Object.clone(ani.endValue)
2427             ani.endValue = Object.sum(ani.difference, ani.endValue)
2428           }
2429           if (ani.repeat == 0) {
2430             shouldRemove = true
2431             pos = 1
2432           } else {
2433             ani.startTime = t
2434             pos = pos % 1
2435           }
2436         }
2437       } else if (ani.repeat && ani.repeat !== true && ani.repeat <= pos) {
2438         shouldRemove = true
2439         pos = ani.repeat
2440       }
2441       this.tweenVariable(ani.variable, ani.startValue, ani.endValue, pos, ani.tween)
2442       if (shouldRemove) {
2443         this.animators.splice(i, 1)
2444         i--
2445       }
2446     }
2447   },
2448 
2449   tweenVariable : function(variable, start, end, pos, tweenFunction) {
2450     if (typeof(tweenFunction) != 'function') {
2451       tweenFunction = this.tweenFunctions[tweenFunction] || this.tweenFunctions.linear
2452     }
2453     var tweened = tweenFunction(pos)
2454     if (typeof(variable) != 'function') {
2455       if (start instanceof Array) {
2456         for (var j=0; j<start.length; j++) {
2457           this[variable][j] = start[j] + tweened*(end[j]-start[j])
2458         }
2459       } else {
2460         this[variable] = start + tweened*(end-start)
2461       }
2462     } else {
2463       variable.call(this, tweened, start, end)
2464     }
2465     this.changed = true
2466   },
2467 
2468   animate : function(variable, start, end, duration, tween, config) {
2469     var start = Object.clone(start)
2470     var end = Object.clone(end)
2471     if (!config) config = {}
2472     if (config.additive) {
2473       var diff = Object.sub(end, start)
2474       start = Object.sum(start, this[variable])
2475       end = Object.sum(end, this[variable])
2476     }
2477     if (typeof(variable) != 'function')
2478       this[variable] = Object.clone(start)
2479     var ani = {
2480       id : Animatable.uid++,
2481       variable : variable,
2482       startValue : start,
2483       endValue : end,
2484       difference : diff,
2485       duration : duration,
2486       tween : tween,
2487       repeat : config.repeat,
2488       additive : config.additive,
2489       accumulate : config.accumulate,
2490       pingpong : config.pingpong
2491     }
2492     this.animators.push(ani)
2493     return ani
2494   },
2495 
2496   removeAnimator : function(animator) {
2497     this.animators.deleteFirst(animator)
2498   },
2499 
2500   animateTo : function(variableName, end, duration, tween, config) {
2501     return this.animate(variableName, this[variableName], end, duration, tween, config)
2502   },
2503 
2504   animateFrom : function(variableName, start, duration, tween, config) {
2505     return this.animate(variableName, start, this[variableName], duration, tween, config)
2506   },
2507 
2508   animateFactor : function(variableName, start, endFactor, duration, tween, config) {
2509     var end
2510     if (start instanceof Array) {
2511       end = []
2512       for (var i=0; i<start.length; i++) {
2513         end[i] = start[i] * endFactor
2514       }
2515     } else {
2516       end = start * endFactor
2517     }
2518     return this.animate(variableName, start, end, duration, tween, config)
2519   },
2520 
2521   animateToFactor : function(variableName, endFactor, duration, tween, config) {
2522     var start = this[variableName]
2523     return this.animateFactor(variableName, start, endFactor, duration, tween, config)
2524   },
2525 
2526   addKeyframe : function(time, target, tween) {
2527     var kf = {
2528       relativeTime: time,
2529       target: target,
2530       tween: tween
2531     }
2532     this.pendingKeyframes.push(kf)
2533   },
2534 
2535   addKeyframeAt : function(time, target, tween) {
2536     var kf = {
2537       time: time,
2538       target: target,
2539       tween: tween
2540     }
2541     this.pendingKeyframes.push(kf)
2542   },
2543 
2544   every : function(duration, action, noFirst) {
2545     var kf = {
2546       action : action,
2547       relativeStartTime : noFirst ? duration : 0,
2548       repeatEvery : duration
2549     }
2550     this.addTimelineEvent(kf)
2551     return kf
2552   },
2553 
2554   at : function(time, action) {
2555     var kf = {
2556       action : action,
2557       startTime : time
2558     }
2559     this.addTimelineEvent(kf)
2560     return kf
2561   },
2562 
2563   after : function(duration, action) {
2564     var kf = {
2565       action : action,
2566       relativeStartTime : duration
2567     }
2568     this.addTimelineEvent(kf)
2569     return kf
2570   },
2571 
2572   afterFrame : function(duration, callback) {
2573     var elapsed = 0
2574     var animator
2575     animator = function(t, dt){
2576       if (elapsed >= duration) {
2577         callback.call(this)
2578         this.removeFrameListener(animator)
2579       }
2580       elapsed++
2581     }
2582     this.addFrameListener(animator)
2583     return animator
2584   },
2585 
2586   everyFrame : function(duration, callback, noFirst) {
2587     var elapsed = noFirst ? 0 : duration
2588     var animator
2589     animator = function(t, dt){
2590       if (elapsed >= duration) {
2591         if (callback.call(this) == false)
2592           this.removeFrameListener(animator)
2593         elapsed = 0
2594       }
2595       elapsed++
2596     }
2597     this.addFrameListener(animator)
2598     return animator
2599   }
2600 })
2601 Animatable.uid = 0
2602 
2603 
2604 
2605 /**
2606   CanvasNode is the base CAKE scenegraph node. All the other scenegraph nodes
2607   derive from it. A plain CanvasNode does no drawing, but it can be used for
2608   grouping other nodes and setting up the group's drawing state.
2609 
2610   var scene = new CanvasNode({x: 10, y: 10})
2611 
2612   The usual way to use CanvasNodes is to append them to a Canvas object:
2613 
2614     var scene = new CanvasNode()
2615     scene.append(new Rectangle(40, 40, {fill: true}))
2616     var elem = E.canvas(400, 400)
2617     var canvas = new Canvas(elem)
2618     canvas.append(scene)
2619 
2620   You can also use CanvasNodes to draw directly to a canvas element:
2621 
2622     var scene = new CanvasNode()
2623     scene.append(new Circle(40, {x:200, y:200, stroke: true}))
2624     var elem = E.canvas(400, 400)
2625     scene.handleDraw(elem.getContext('2d'))
2626 
2627   */
2628 CanvasNode = Klass(Animatable, Transformable, {
2629   OBJECTBOUNDINGBOX : 'objectBoundingBox',
2630 
2631   // whether to draw the node and its childNodes or not
2632   visible : true,
2633 
2634   // whether to draw the node (doesn't affect subtree)
2635   drawable : true,
2636 
2637   // the CSS display property can be used to affect 'visible'
2638   // false     => visible = visible
2639   // 'none'    => visible = false
2640   // otherwise => visible = true
2641   display : null,
2642 
2643   // the CSS visibility property can be used to affect 'drawable'
2644   // false     => drawable = drawable
2645   // 'hidden'  => drawable = false
2646   // otherwise => drawable = true
2647   visibility : null,
2648 
2649   // whether this and the subtree from this register mouse hover
2650   catchMouse : true,
2651 
2652   // Whether this object registers mouse hover. Only set this to true when you
2653   // have a drawable object that can be picked. Otherwise the object requires
2654   // a matrix inversion on Firefox 2 and Safari, which is slow.
2655   pickable : false,
2656 
2657   // true if this node or one of its descendants is under the mouse
2658   // cursor and catchMouse is true
2659   underCursor : false,
2660 
2661   // zIndex in relation to sibling nodes (note: not global)
2662   zIndex : 0,
2663 
2664   // x translation of the node
2665   x : 0,
2666 
2667   // y translation of the node
2668   y : 0,
2669 
2670   // scale factor: number for uniform scaling, [x,y] for dimension-wise
2671   scale : 1,
2672 
2673   // Rotation of the node, in radians.
2674   //
2675   // The rotation can also be the array [angle, cx, cy],
2676   // where cx and cy define the rotation center.
2677   //
2678   // The array form is equivalent to
2679   // translate(cx, cy); rotate(angle); translate(-cx, -cy);
2680   rotation : 0,
2681 
2682   // Transform matrix with which to multiply the current transform matrix.
2683   // Applied after all other transformations.
2684   matrix : null,
2685 
2686   // Transform matrix with which to replace the current transform matrix.
2687   // Applied before any other transformation.
2688   absoluteMatrix : null,
2689 
2690   // SVG-like list of transformations to apply.
2691   // The different transformations are:
2692   // ['translate', [x,y]]
2693   // ['rotate', [angle, cx, cy]] - (optional) cx and cy are the rotation center
2694   // ['scale', [x,y]]
2695   // ['matrix', [m11, m12, m21, m22, dx, dy]]
2696   transformList : null,
2697 
2698   // fillStyle for the node and its descendants
2699   // Possibilities:
2700   //   null // use the previous
2701   //   true      // use the previous but do fill
2702   //   false     // use the previous but don't do fill
2703   //   'none'    // use the previous but don't do fill
2704   //
2705   //   'white'
2706   //   '#fff'
2707   //   '#ffffff'
2708   //   'rgba(255,255,255, 1.0)'
2709   //   [255, 255, 255, 1.0]
2710   //   new Gradient(...)
2711   //   new Pattern(myImage, 'no-repeat')
2712   fill : null,
2713 
2714   // strokeStyle for the node and its descendants
2715   // Possibilities:
2716   //   null // use the previous
2717   //   true      // use the previous but do stroke
2718   //   false     // use the previous but don't do stroke
2719   //   'none'    // use the previous but don't do stroke
2720   //
2721   //   'white'
2722   //   '#fff'
2723   //   '#ffffff'
2724   //   'rgba(255,255,255, 1.0)'
2725   //   [255, 255, 255, 1.0]
2726   //   new Gradient(...)
2727   //   new Pattern(myImage, 'no-repeat')
2728   stroke : null,
2729 
2730   // stroke line width
2731   strokeWidth : null,
2732 
2733   // stroke line cap style ('butt' | 'round' | 'square')
2734   lineCap : null,
2735 
2736   // stroke line join style ('bevel' | 'round' | 'miter')
2737   lineJoin : null,
2738 
2739   // stroke line miter limit
2740   miterLimit : null,
2741 
2742   // set globalAlpha to this value
2743   absoluteOpacity : null,
2744 
2745   // multiply globalAlpha by this value
2746   opacity : null,
2747 
2748   // fill opacity
2749   fillOpacity : null,
2750 
2751   // stroke opacity
2752   strokeOpacity : null,
2753 
2754   // set globalCompositeOperation to this value
2755   // Possibilities:
2756   // ( 'source-over' |
2757   //   'copy' |
2758   //   'lighter' |
2759   //   'darker' |
2760   //   'xor' |
2761   //   'source-in' |
2762   //   'source-out' |
2763   //   'destination-over' |
2764   //   'destination-atop' |
2765   //   'destination-in' |
2766   //   'destination-out' )
2767   compositeOperation : null,
2768 
2769   // CSS style for the text (this is volatile, there is no canvas text spec)
2770   textStyle : null,
2771 
2772   // Color for the drop shadow
2773   shadowColor : null,
2774 
2775   // Drop shadow blur radius
2776   shadowBlur : null,
2777 
2778   // Drop shadow's x-offset
2779   shadowOffsetX : null,
2780 
2781   // Drop shadow's y-offset
2782   shadowOffsetY : null,
2783 
2784   // Used by Firefox MozDrawText canvas extension for setting the text style.
2785   // CSS font style
2786   mozTextStyle : null,
2787 
2788   // Perfect world text API
2789   // CSS font style
2790   textStyle : null,
2791   // horizontal position of the text origin
2792   // 'left' | 'center' | 'right'
2793   textAlign : null,
2794   // vertical position of the text origin
2795   // 'top' | 'baseline' | 'bottom'
2796   textVAlign : null,
2797 
2798   cursor : null,
2799 
2800   changed : true,
2801 
2802   tagName : 'g',
2803 
2804   getNextSibling : function(){
2805     if (this.parentNode)
2806       return this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this)+1]
2807     return null
2808   },
2809 
2810   getPreviousSibling : function(){
2811     if (this.parentNode)
2812       return this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this)-1]
2813     return null
2814   },
2815 
2816   /**
2817     Initialize the CanvasNode and merge an optional config hash.
2818     */
2819   initialize : function(config) {
2820     this.root = this
2821     this.currentMatrix = [1,0,0,1,0,0]
2822     this.previousMatrix = [1,0,0,1,0,0]
2823     this.needMatrixUpdate = true
2824     this.childNodes = []
2825     this.frameListeners = []
2826     this.eventListeners = {}
2827     Animatable.initialize.call(this)
2828     if (config)
2829       Object.extend(this, config)
2830   },
2831 
2832   /**
2833     Create a clone of the node and its subtree.
2834     */
2835   clone : function() {
2836     var c = Object.clone(this)
2837     c.parent = c.root = null
2838     for (var i in this) {
2839       if (typeof(this[i]) == 'object')
2840         c[i] = Object.clone(this[i])
2841     }
2842     c.parent = c.root = null
2843     c.childNodes = []
2844     c.setRoot(null)
2845     for (var i=0; i<this.childNodes.length; i++) {
2846       var ch = this.childNodes[i].clone()
2847       c.append(ch)
2848     }
2849     return c
2850   },
2851 
2852   cloneNode : function(){ return this.clone() },
2853 
2854   /**
2855     Gets node by id.
2856     */
2857   getElementById : function(id) {
2858     if (this.id == id)
2859       return this
2860     for (var i=0; i<this.childNodes.length; i++) {
2861       var n = this.childNodes[i].getElementById(id)
2862       if (n) return n
2863     }
2864     return null
2865   },
2866 
2867   $ : function(id) {
2868     return this.getElementById(id)
2869   },
2870 
2871   /**
2872     Alias for append().
2873 
2874     @param Node[s] to append
2875     */
2876   appendChild : function() {
2877     return this.append.apply(this, arguments)
2878   },
2879 
2880   /**
2881     Appends arguments as childNodes to the node.
2882 
2883     Adding a child sets child.parent to be the node and calls
2884     child.setRoot(node.root)
2885 
2886     @param Node[s] to append
2887     */
2888   append : function(obj) {
2889     var a = $A(arguments)
2890     for (var i=0; i<a.length; i++) {
2891       if (a[i].parent) a[i].removeSelf()
2892       this.childNodes.push(a[i])
2893       a[i].parent = a[i].parentNode = this
2894       a[i].setRoot(this.root)
2895     }
2896     this.changed = true
2897   },
2898 
2899   /**
2900     Removes all childNodes from the node.
2901     */
2902   removeAllChildren : function() {
2903     this.remove.apply(this, this.childNodes)
2904   },
2905 
2906   /**
2907     Alias for remove().
2908 
2909     @param Node[s] to remove
2910     */
2911   removeChild : function() {
2912     return this.remove.apply(this, arguments)
2913   },
2914 
2915   /**
2916     Removes arguments from the node's childNodes.
2917 
2918     Removing a child sets its parent to null and calls
2919     child.setRoot(null)
2920 
2921     @param Child node[s] to remove
2922     */
2923   remove : function(obj) {
2924     var a = arguments
2925     for (var i=0; i<a.length; i++) {
2926       this.childNodes.deleteFirst(a[i])
2927       delete a[i].parent
2928       delete a[i].parentNode
2929       a[i].setRoot(null)
2930     }
2931     this.changed = true
2932   },
2933 
2934   /**
2935     Calls this.parent.removeChild(this) if this.parent is set.
2936     */
2937   removeSelf : function() {
2938     if (this.parentNode) {
2939       this.parentNode.remove(this)
2940     }
2941   },
2942 
2943   /**
2944     Returns true if this node's subtree contains obj. (I.e. obj is this or
2945     obj's parent chain includes this.)
2946 
2947     @param obj Node to look for
2948     @return True if obj is in this node's subtree, false if it isn't.
2949     */
2950   contains : function(obj) {
2951     while (obj) {
2952       if (obj == this) return true
2953       obj = obj.parentNode
2954     }
2955     return false
2956   },
2957 
2958   /**
2959     Set this.root to the given value and propagate the update to childNodes.
2960 
2961     @param root The new root node
2962     @private
2963     */
2964   setRoot : function(root) {
2965     if (!root) root = this
2966     this.dispatchEvent({type: 'rootChanged', canvasTarget: this, relatedTarget: root})
2967     this.root = root
2968     for (var i=0; i<this.childNodes.length; i++)
2969       this.childNodes[i].setRoot(root)
2970   },
2971 
2972   /**
2973     Adds a callback function to be called before drawing each frame.
2974 
2975     @param f Callback function
2976     */
2977   addFrameListener : function(f) {
2978     this.frameListeners.push(f)
2979   },
2980 
2981   /**
2982     Removes a callback function from update callbacks.
2983 
2984     @param f Callback function
2985     */
2986   removeFrameListener : function(f) {
2987     this.frameListeners.deleteFirst(f)
2988   },
2989 
2990   addEventListener : function(type, listener, capture) {
2991     if (!this.eventListeners[type])
2992       this.eventListeners[type] = {capture:[], bubble:[]}
2993     this.eventListeners[type][capture ? 'capture' : 'bubble'].push(listener)
2994   },
2995 
2996   /**
2997     Synonym for addEventListener.
2998   */
2999   when : function(type, listener, capture) {
3000     this.addEventListener(type, listener, capture || false)
3001   },
3002 
3003   removeEventListener : function(type, listener, capture) {
3004     if (!this.eventListeners[type]) return
3005     this.eventListeners[type][capture ? 'capture' : 'bubble'].deleteFirst(listener)
3006     if (this.eventListeners[type].capture.length == 0 &&
3007         this.eventListeners[type].bubble.length == 0)
3008       delete this.eventListeners[type]
3009   },
3010 
3011   dispatchEvent : function(event) {
3012     var type = event.type
3013     if (!event.canvasTarget) {
3014       if (type.search(/^(key|text)/i) == 0) {
3015         event.canvasTarget = this.root.focused || this.root.target
3016       } else {
3017         event.canvasTarget = this.root.target
3018       }
3019       if (!event.canvasTarget)
3020         event.canvasTarget = this
3021     }
3022     var path = []
3023     var obj = event.canvasTarget
3024     while (obj && obj != this) {
3025       path.push(obj)
3026       obj = obj.parent
3027     }
3028     path.push(this)
3029     event.canvasPhase = 'capture'
3030     for (var i=path.length-1; i>=0; i--)
3031       if (!path[i].handleEvent(event)) return false
3032     event.canvasPhase = 'bubble'
3033     for (var i=0; i<path.length; i++)
3034       if (!path[i].handleEvent(event)) return false
3035     return true
3036   },
3037 
3038   broadcastEvent : function(event) {
3039     var type = event.type
3040     event.canvasPhase = 'capture'
3041     if (!this.handleEvent(event)) return false
3042     for (var i=0; i<this.childNodes.length; i++)
3043       if (!this.childNodes[i].broadcastEvent(event)) return false
3044     event.canvasPhase = 'bubble'
3045     if (!this.handleEvent(event)) return false
3046     return true
3047   },
3048 
3049   handleEvent : function(event) {
3050     var type = event.type
3051     var phase = event.canvasPhase
3052     if (this.cursor && phase == 'capture')
3053       event.cursor = this.cursor
3054     var els = this.eventListeners[type]
3055     els = els && els[phase]
3056     if (els) {
3057       for (var i=0; i<els.length; i++) {
3058         var rv = els[i].call(this, event)
3059         if (rv == false || event.stopped) {
3060           if (!event.stopped)
3061             event.stopPropagation()
3062           event.stopped = true
3063           return false
3064         }
3065       }
3066     }
3067     return true
3068   },
3069 
3070   /**
3071     Handle scenegraph update.
3072     Called with current time before drawing each frame.
3073 
3074     This method should be touched only if you know what you're doing.
3075     If you need your own update handler, either add a frame listener or
3076     overwrite {@link CanvasNode#update}.
3077 
3078     @param time Current animation time
3079     @param timeDelta Time since last frame in milliseconds
3080     */
3081   handleUpdate : function(time, timeDelta) {
3082     this.update(time, timeDelta)
3083     this.willBeDrawn = (!this.parent || this.parent.willBeDrawn) && (this.display ? this.display != 'none' : this.visible)
3084     for(var i=0; i<this.childNodes.length; i++)
3085       this.childNodes[i].handleUpdate(time, timeDelta)
3086     // TODO propagate dirty area bbox up the scene graph
3087     if (this.parent && this.changed) {
3088       this.parent.changed = this.changed
3089       this.changed = false
3090     }
3091     this.needMatrixUpdate = true
3092   },
3093 
3094   /**
3095     Update this node. Calls all frame listener callbacks in the order they
3096     were added.
3097 
3098     Overwrite this with your own method if you want to do things differently.
3099 
3100     @param time Current animation time
3101     @param timeDelta Time since last frame in milliseconds
3102     */
3103   update : function(time, timeDelta) {
3104     // need to operate on a copy, otherwise bad stuff happens
3105     var fl = this.frameListeners.slice(0)
3106     for(var i=0; i<fl.length; i++) {
3107       fl[i].apply(this, arguments)
3108     }
3109   },
3110 
3111   /**
3112     Tests if this node or its subtree is under the mouse cursor and
3113     sets this.underCursor accordingly.
3114 
3115     If this node (and not one of its childNodes) is under the mouse cursor
3116     this.root.target is set to this. This way, the topmost (== drawn last)
3117     node under the mouse cursor is the root target.
3118 
3119     To see whether a subtree node is the current target:
3120 
3121     if (this.underCursor && this.contains(this.root.target)) {
3122       // we are the target, let's roll
3123     }
3124 
3125     This method should be touched only if you know what you're doing.
3126     Overwrite {@link CanvasNode#drawPickingPath} to change the way the node's
3127     picking path is created.
3128 
3129     Called after handleUpdate, but before handleDraw.
3130 
3131     @param ctx Canvas 2D context
3132     */
3133   handlePick : function(ctx) {
3134     // CSS display & visibility
3135     if (this.display)
3136       this.visible = (this.display != 'none')
3137     if (this.visibility)
3138       this.drawable = (this.visibility != 'hidden')
3139     this.underCursor = false
3140     if (this.visible && this.catchMouse && this.root.absoluteMouseX != null) {
3141       ctx.save()
3142       this.transform(ctx, true)
3143       if (this.pickable && this.drawable) {
3144         if (ctx.isPointInPath) {
3145           ctx.beginPath()
3146           if (this.drawPickingPath)
3147             this.drawPickingPath(ctx)
3148         }
3149         this.underCursor = CanvasSupport.isPointInPath(
3150                               this.drawPickingPath ? ctx : false,
3151                               this.root.mouseX,
3152                               this.root.mouseY,
3153                               this.currentMatrix,
3154                               this)
3155         if (this.underCursor)
3156           this.root.target = this
3157       } else {
3158         this.underCursor = false
3159       }
3160       var c = this.__getChildrenCopy()
3161       c.sort(this.__zIndexSort)
3162       for(var i=0; i<c.length; i++) {
3163         c[i].handlePick(ctx)
3164         if (!this.underCursor)
3165           this.underCursor = c[i].underCursor
3166       }
3167       ctx.restore()
3168     } else {
3169       var c = this.__getChildrenCopy()
3170       while (c.length > 0) {
3171         var c0 = c.pop()
3172         if (c0.underCursor) {
3173           c0.underCursor = false
3174           Array.prototype.push.apply(c, c0.childNodes)
3175         }
3176       }
3177     }
3178   },
3179 
3180   __zIndexSort : function(c1,c2){
3181     return c1.zIndex - c2.zIndex
3182   },
3183 
3184   __getChildrenCopy : function() {
3185     if (this.__childNodesCopy) {
3186       while (this.__childNodesCopy.length > this.childNodes.length)
3187         this.__childNodesCopy.pop()
3188       for (var i=0; i<this.childNodes.length; i++)
3189         this.__childNodesCopy[i] = this.childNodes[i]
3190     } else {
3191       this.__childNodesCopy = this.childNodes.slice(0)
3192     }
3193     return this.__childNodesCopy
3194   },
3195 
3196   /**
3197     Returns true if the point x,y is inside the path of a drawable node.
3198 
3199     The x,y point is in user-space coordinates, meaning that e.g. the point
3200     5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
3201     transform on the rectangle.
3202 
3203     Leave isPointInPath to false to avoid unnecessary matrix inversions for
3204     non-drawables.
3205 
3206     @param x X-coordinate of the point.
3207     @param y Y-coordinate of the point.
3208     @return Whether the point is inside the path of this node.
3209     @type boolean
3210     */
3211   isPointInPath : false,
3212 
3213   /**
3214     Handles transforming and drawing the node and its childNodes
3215     on each frame.
3216 
3217     Pushes context state, applies state transforms and draws the node.
3218     Then sorts the node's childNodes by zIndex, smallest first, and
3219     calls their handleDraws in that order. Finally, pops the context state.
3220 
3221     Called after handleUpdate and handlePick.
3222 
3223     This method should be touched only if you know what you're doing.
3224     Overwrite {@link CanvasNode#draw} when you need to draw things.
3225 
3226     @param ctx Canvas 2D context
3227     */
3228   handleDraw : function(ctx) {
3229     // CSS display & visibility
3230     if (this.display)
3231       this.visible = (this.display != 'none')
3232     if (this.visibility)
3233       this.drawable = (this.visibility != 'hidden')
3234     if (!this.visible) return
3235     ctx.save()
3236     var pff = ctx.fontFamily
3237     var pfs = ctx.fontSize
3238     var pfo = ctx.fillOn
3239     var pso = ctx.strokeOn
3240     if (this.fontFamily)
3241       ctx.fontFamily = this.fontFamily
3242     if (this.fontSize)
3243       ctx.fontSize = this.fontSize
3244     this.transform(ctx)
3245     if (this.clipPath) {
3246       ctx.beginPath()
3247       if (this.clipPath.units == this.OBJECTBOUNDINGBOX) {
3248         var bb = this.getSubtreeBoundingBox(true)
3249         ctx.save()
3250         ctx.translate(bb[0], bb[1])
3251         ctx.scale(bb[2], bb[3])
3252         this.clipPath.createSubtreePath(ctx, true)
3253         ctx.restore()
3254         ctx.clip()
3255       } else {
3256         this.clipPath.createSubtreePath(ctx, true)
3257         ctx.clip()
3258       }
3259     }
3260     if (this.drawable && this.draw)
3261       this.draw(ctx)
3262     var c = this.__getChildrenCopy()
3263     c.sort(this.__zIndexSort)
3264     for(var i=0; i<c.length; i++) {
3265       c[i].handleDraw(ctx)
3266     }
3267     ctx.fontFamily = pff
3268     ctx.fontSize = pfs
3269     ctx.fillOn = pfo
3270     ctx.strokeOn = pso
3271     ctx.restore()
3272   },
3273 
3274   /**
3275     Transforms the context state according to this node's attributes.
3276 
3277     @param ctx Canvas 2D context
3278     @param onlyTransform If set to true, only do matrix transforms.
3279     */
3280   transform : function(ctx, onlyTransform) {
3281     Transformable.prototype.transform.call(this, ctx)
3282 
3283     if (onlyTransform) return
3284 
3285     // stroke / fill modifiers
3286     if (this.fill != null) {
3287       if (!this.fill || this.fill == 'none') {
3288         ctx.fillOn = false
3289       } else {
3290         ctx.fillOn = true
3291         if (this.fill != true) {
3292           var fillStyle = Colors.parseColorStyle(this.fill, ctx)
3293           ctx.setFillStyle( fillStyle )
3294         }
3295       }
3296     }
3297     if (this.stroke != null) {
3298       if (!this.stroke || this.stroke == 'none') {
3299         ctx.strokeOn = false
3300       } else {
3301         ctx.strokeOn = true
3302         if (this.stroke != true)
3303           ctx.setStrokeStyle( Colors.parseColorStyle(this.stroke, ctx) )
3304       }
3305     }
3306     if (this.strokeWidth != null)
3307       ctx.setLineWidth( this.strokeWidth )
3308     if (this.lineCap != null)
3309       ctx.setLineCap( this.lineCap )
3310     if (this.lineJoin != null)
3311       ctx.setLineJoin( this.lineJoin )
3312     if (this.miterLimit != null)
3313       ctx.setMiterLimit( this.miterLimit )
3314 
3315     // compositing modifiers
3316     if (this.absoluteOpacity != null)
3317       ctx.setGlobalAlpha( this.absoluteOpacity )
3318     if (this.opacity != null)
3319       ctx.setGlobalAlpha( ctx.globalAlpha * this.opacity )
3320     if (this.compositeOperation != null)
3321       ctx.setGlobalCompositeOperation( this.compositeOperation )
3322 
3323     // shadow modifiers
3324     if (this.shadowColor != null)
3325       ctx.setShadowColor( Colors.parseColorStyle(this.shadowColor, ctx) )
3326     if (this.shadowBlur != null)
3327       ctx.setShadowBlur( this.shadowBlur )
3328     if (this.shadowOffsetX != null)
3329       ctx.setShadowOffsetX( this.shadowOffsetX )
3330     if (this.shadowOffsetY != null)
3331       ctx.setShadowOffsetY( this.shadowOffsetY )
3332 
3333     // text modifiers
3334     if (this.textStyle != null)
3335       ctx.setMozTextStyle( this.textStyle )
3336   },
3337 
3338   /**
3339     Draws the picking path for the node for testing if the mouse cursor
3340     is inside the node.
3341 
3342     False by default, overwrite if you need special behaviour.
3343 
3344     @param ctx Canvas 2D context
3345     */
3346   drawPickingPath : false,
3347 
3348   /**
3349     Draws the node.
3350 
3351     False by default, overwrite to actually draw something.
3352 
3353     @param ctx Canvas 2D context
3354     */
3355   draw : false,
3356 
3357   createSubtreePath : function(ctx, skipTransform) {
3358     ctx.save()
3359     if (!skipTransform) this.transform(ctx, true)
3360     for (var i=0; i<this.childNodes.length; i++)
3361       this.childNodes[i].createSubtreePath(ctx)
3362     ctx.restore()
3363   },
3364 
3365   getSubtreeBoundingBox : function(identity) {
3366     if (identity) {
3367       var p = this.parent
3368       this.parent = null
3369       this.needMatrixUpdate = true
3370     }
3371     var bb = this.getAxisAlignedBoundingBox()
3372     for (var i=0; i<this.childNodes.length; i++) {
3373       var cbb = this.childNodes[i].getSubtreeBoundingBox()
3374       if (!bb) {
3375         bb = cbb
3376       } else if (cbb) {
3377         this.mergeBoundingBoxes(bb, cbb)
3378       }
3379     }
3380     if (identity) {
3381       this.parent = p
3382       this.needMatrixUpdate = true
3383     }
3384     return bb
3385   },
3386 
3387   mergeBoundingBoxes : function(bb, bb2) {
3388     if (bb[0] > bb2[0]) bb[0] = bb2[0]
3389     if (bb[1] > bb2[1]) bb[1] = bb2[1]
3390     if (bb[2]+bb[0] < bb2[2]+bb2[0]) bb[2] = bb2[2]+bb2[0]-bb[0]
3391     if (bb[3]+bb[1] < bb2[3]+bb2[1]) bb[3] = bb2[3]+bb2[1]-bb[1]
3392   },
3393 
3394   getAxisAlignedBoundingBox : function() {
3395     this.transform(null, true)
3396     if (!this.getBoundingBox) return null
3397     var bbox = this.getBoundingBox()
3398     var xy1 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
3399       bbox[0], bbox[1])
3400     var xy2 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
3401       bbox[0]+bbox[2], bbox[1]+bbox[3])
3402     var xy3 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
3403       bbox[0], bbox[1]+bbox[3])
3404     var xy4 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix,
3405       bbox[0]+bbox[2], bbox[1])
3406     var x1 = Math.min(xy1[0], xy2[0], xy3[0], xy4[0])
3407     var x2 = Math.max(xy1[0], xy2[0], xy3[0], xy4[0])
3408     var y1 = Math.min(xy1[1], xy2[1], xy3[1], xy4[1])
3409     var y2 = Math.max(xy1[1], xy2[1], xy3[1], xy4[1])
3410     return [x1, y1, x2-x1, y2-y1]
3411   }
3412 
3413 })
3414 
3415 
3416 /**
3417   Canvas is the canvas manager class.
3418   It takes care of updating and drawing its childNodes on a canvas element.
3419 
3420   An example with a rotating rectangle:
3421 
3422     var c = E.canvas(500, 500)
3423     var canvas = new Canvas(c)
3424     var rect = new Rectangle(100, 100)
3425     rect.x = 250
3426     rect.y = 250
3427     rect.fill = true
3428     rect.fillStyle = 'green'
3429     rect.addFrameListener(function(t) {
3430       this.rotation = ((t / 3000) % 1) * Math.PI * 2
3431     })
3432     canvas.append(rect)
3433     document.body.appendChild(c)
3434 
3435 
3436   To use the canvas as a manually updated image:
3437 
3438     var canvas = new Canvas(E.canvas(200,40), {
3439       isPlaying : false,
3440       redrawOnlyWhenChanged : true
3441     })
3442     var c = new Circle(20)
3443     c.x = 100
3444     c.y = 20
3445     c.fill = true
3446     c.fillStyle = 'red'
3447     c.addFrameListener(function(t) {
3448       if (this.root.absoluteMouseX != null) {
3449         this.x = this.root.mouseX // relative to canvas surface
3450         this.root.changed = true
3451       }
3452     })
3453     canvas.append(c)
3454 
3455 
3456   Or by using raw onFrame-calls:
3457 
3458     var canvas = new Canvas(E.canvas(200,40), {
3459       isPlaying : false,
3460       fill : true,
3461       fillStyle : 'white'
3462     })
3463     var c = new Circle(20)
3464     c.x = 100
3465     c.y = 20
3466     c.fill = true
3467     c.fillStyle = 'red'
3468     canvas.append(c)
3469     canvas.onFrame()
3470 
3471 
3472   Which is also the recommended way to use a canvas inside another canvas:
3473 
3474     var canvas = new Canvas(E.canvas(200,40), {
3475       isPlaying : false
3476     })
3477     var c = new Circle(20, {
3478       x: 100, y: 20,
3479       fill: true, fillStyle: 'red'
3480     })
3481     canvas.append(c)
3482 
3483     var topCanvas = new Canvas(E.canvas(500, 500))
3484     var canvasImage = new ImageNode(canvas.canvas, {x: 250, y: 250})
3485     topCanvas.append(canvasImage)
3486     canvasImage.addFrameListener(function(t) {
3487       this.rotation = (t / 3000 % 1) * Math.PI * 2
3488       canvas.onFrame(t)
3489     })
3490 
3491   */
3492 Canvas = Klass(CanvasNode, {
3493 
3494   clear : true,
3495   frameLoop : false,
3496   recording : false,
3497   opacity : 1,
3498   frame : 0,
3499   elapsed : 0,
3500   frameDuration : 30,
3501   speed : 1.0,
3502   time : 0,
3503   fps : 0,
3504   currentRealFps : 0,
3505   currentFps : 0,
3506   fpsFrames : 30,
3507   startTime : 0,
3508   realFps : 0,
3509   fixedTimestep : false,
3510   playOnlyWhenFocused : true,
3511   isPlaying : true,
3512   redrawOnlyWhenChanged : false,
3513   changed : true,
3514   drawBoundingBoxes : false,
3515   cursor : 'default',
3516 
3517   mouseDown : false,
3518   mouseEvents : [],
3519 
3520   // absolute pixel coordinates from canvas top-left
3521   absoluteMouseX : null,
3522   absoluteMouseY : null,
3523 
3524   /*
3525     Coordinates relative to the canvas's surface scale.
3526     Example:
3527       canvas.width
3528       #=> 100
3529       canvas.style.width
3530       #=> '100px'
3531       canvas.absoluteMouseX
3532       #=> 50
3533       canvas.mouseX
3534       #=> 50
3535 
3536       canvas.style.width = '200px'
3537       canvas.width
3538       #=> 100
3539       canvas.absoluteMouseX
3540       #=> 100
3541       canvas.mouseX
3542       #=> 50
3543   */
3544   mouseX : null,
3545   mouseY : null,
3546 
3547   initialize : function(canvas, config) {
3548     if (arguments.length > 2) {
3549       var container = arguments[0]
3550       var w = arguments[1]
3551       var h = arguments[2]
3552       var config = arguments[3]
3553       var canvas = E.canvas(w,h)
3554       var canvasContainer = E('div', canvas, {style:
3555         {overflow:'hidden', width:w+'px', height:h+'px', position:'relative'}
3556       })
3557       this.canvasContainer = canvasContainer
3558       if (container)
3559         container.appendChild(canvasContainer)
3560     }
3561     CanvasNode.initialize.call(this, config)
3562     this.mouseEventStack = []
3563     this.canvas = canvas
3564     canvas.canvas = this
3565     this.width = this.canvas.width
3566     this.height = this.canvas.height
3567     var th = this
3568     this.frameHandler = function() { th.onFrame() }
3569     this.canvas.addEventListener('DOMNodeInserted', function(ev) {
3570       if (ev.target == this)
3571         th.addEventListeners()
3572     }, false)
3573     this.canvas.addEventListener('DOMNodeRemoved', function(ev) {
3574       if (ev.target == this)
3575         th.removeEventListeners()
3576     }, false)
3577     if (this.canvas.parentNode) this.addEventListeners()
3578     this.startTime = new Date().getTime()
3579     if (this.isPlaying)
3580       this.play()
3581   },
3582 
3583   // FIXME
3584   removeEventListeners : function() {
3585   },
3586 
3587   addEventListeners : function() {
3588     var th = this
3589     this.canvas.parentNode.addMouseEvent = function(e){
3590       var xy = Mouse.getRelativeCoords(this, e)
3591       th.absoluteMouseX = xy.x
3592       th.absoluteMouseY = xy.y
3593       var style = document.defaultView.getComputedStyle(th.canvas,"")
3594       var w = parseFloat(style.getPropertyValue('width'))
3595       var h = parseFloat(style.getPropertyValue('height'))
3596       th.mouseX = th.absoluteMouseX * (w / th.canvas.width)
3597       th.mouseY = th.absoluteMouseY * (h / th.canvas.height)
3598       th.addMouseEvent(th.mouseX, th.mouseY, th.mouseDown)
3599     }
3600     this.canvas.parentNode.contains = this.contains
3601 
3602     this.canvas.parentNode.addEventListener('mousedown', function(e) {
3603       th.mouseDown = true
3604       if (th.keyTarget != th.target) {
3605         if (th.keyTarget)
3606           th.dispatchEvent({type: 'blur', canvasTarget: th.keyTarget})
3607         th.keyTarget = th.target
3608         if (th.keyTarget)
3609           th.dispatchEvent({type: 'focus', canvasTarget: th.keyTarget})
3610       }
3611       this.addMouseEvent(e)
3612     }, true)
3613 
3614     this.canvas.parentNode.addEventListener('mouseup', function(e) {
3615       this.addMouseEvent(e)
3616       th.mouseDown = false
3617     }, true)
3618 
3619     this.canvas.parentNode.addEventListener('mousemove', function(e) {
3620       this.addMouseEvent(e)
3621       if (th.prevClientX == null) {
3622         th.prevClientX = e.clientX
3623         th.prevClientY = e.clientY
3624       }
3625       if (th.dragTarget) {
3626         var nev = document.createEvent('MouseEvents')
3627         nev.initMouseEvent('drag', true, true, window, e.detail,
3628           e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
3629           e.shiftKey, e.metaKey, e.button, e.relatedTarget)
3630         nev.canvasTarget = th.dragTarget
3631         nev.dx = e.clientX - th.prevClientX
3632         nev.dy = e.clientY - th.prevClientY
3633         th.dispatchEvent(nev)
3634       }
3635       if (!th.mouseDown) {
3636         if (th.dragTarget) {
3637           var nev = document.createEvent('MouseEvents')
3638           nev.initMouseEvent('dragend', true, true, window, e.detail,
3639             e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
3640             e.shiftKey, e.metaKey, e.button, e.relatedTarget)
3641           nev.canvasTarget = th.dragTarget
3642           th.dispatchEvent(nev)
3643           th.dragTarget = false
3644         }
3645       } else if (!th.dragTarget && th.target) {
3646         th.dragTarget = th.target
3647         var nev = document.createEvent('MouseEvents')
3648         nev.initMouseEvent('dragstart', true, true, window, e.detail,
3649           e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
3650           e.shiftKey, e.metaKey, e.button, e.relatedTarget)
3651         nev.canvasTarget = th.dragTarget
3652         th.dispatchEvent(nev)
3653       }
3654       th.prevClientX = e.clientX
3655       th.prevClientY = e.clientY
3656     }, true)
3657 
3658     this.canvas.parentNode.addEventListener('mouseout', function(e) {
3659       if (!CanvasNode.contains.call(this, e.relatedTarget))
3660         th.absoluteMouseX = th.absoluteMouseY = th.mouseX = th.mouseY = null
3661     }, true)
3662 
3663     var dispatch = this.dispatchEvent.bind(this)
3664     var types = [
3665       'mousemove', 'mouseover', 'mouseout', 'mousewheel',
3666       'click', 'dblclick',
3667       'mousedown', 'mouseup',
3668       'keypress', 'keydown', 'keyup',
3669       'mousemultiwheel', 'textInput',
3670       'focus', 'blur'
3671     ]
3672     for (var i=0; i<types.length; i++) {
3673       this.canvas.parentNode.addEventListener(types[i], dispatch, false)
3674     }
3675     this.keys = {}
3676 
3677     this.windowEventListeners = {
3678 
3679       keydown : function(ev) {
3680         if (th.keyTarget) {
3681           th.updateKeys(ev)
3682           ev.canvasTarget = th.keyTarget
3683           th.dispatchEvent(ev)
3684         }
3685       },
3686 
3687       keyup : function(ev) {
3688         if (th.keyTarget) {
3689           th.updateKeys(ev)
3690           ev.canvasTarget = th.keyTarget
3691           th.dispatchEvent(ev)
3692         }
3693       },
3694 
3695       // do we even want to have this?
3696       keypress : function(ev) {
3697         if (th.keyTarget) {
3698           ev.canvasTarget = th.keyTarget
3699           th.dispatchEvent(ev)
3700         }
3701       },
3702 
3703       blur : function(ev) {
3704         th.absoluteMouseX = th.absoluteMouseY = null
3705         if (th.playOnlyWhenFocused && th.isPlaying) {
3706           th.stop()
3707           th.__blurStop = true
3708         }
3709       },
3710 
3711       focus : function(ev) {
3712         if (th.__blurStop && !th.isPlaying) th.play()
3713       },
3714 
3715       mouseup : function(e) {
3716         th.mouseDown = false
3717         if (th.dragTarget) {
3718           // TODO
3719           // find the object that receives the drag (i.e. drop target)
3720           var nev = document.createEvent('MouseEvents')
3721           nev.initMouseEvent('dragend', true, true, window, e.detail,
3722             e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey,
3723             e.shiftKey, e.metaKey, e.button, e.relatedTarget)
3724           nev.canvasTarget = th.dragTarget
3725           th.dispatchEvent(nev)
3726           th.dragTarget = false
3727         }
3728         if (!th.canvas.parentNode.contains(e.target)) {
3729           var rv = th.dispatchEvent(e)
3730           if (th.keyTarget) {
3731             th.dispatchEvent({type: 'blur', canvasTarget: th.keyTarget})
3732             th.keyTarget = null
3733           }
3734           return rv
3735         }
3736       },
3737 
3738       mousemove : function(ev) {
3739         if (th.__blurStop && !th.isPlaying) th.play()
3740         if (!th.canvas.parentNode.contains(ev.target) && th.mouseDown)
3741           return th.dispatchEvent(ev)
3742       }
3743 
3744     }
3745 
3746     this.canvas.parentNode.addEventListener('DOMNodeRemoved', function(ev) {
3747       if (ev.target == this)
3748         th.removeWindowEventListeners()
3749     }, false)
3750     this.canvas.parentNode.addEventListener('DOMNodeInserted', function(ev) {
3751       if (ev.target == this)
3752         th.addWindowEventListeners()
3753     }, false)
3754     if (this.canvas.parentNode.parentNode) this.addWindowEventListeners()
3755   },
3756 
3757   updateKeys : function(ev) {
3758     this.keys.shift = ev.shiftKey
3759     this.keys.ctrl = ev.ctrlKey
3760     this.keys.alt = ev.altKey
3761     this.keys.meta = ev.metaKey
3762     var state = (ev.type == 'keydown')
3763     switch (ev.keyCode) {
3764       case 37: this.keys.left = state; break
3765       case 38: this.keys.up = state; break
3766       case 39: this.keys.right = state; break
3767       case 40: this.keys.down = state; break
3768       case 32: this.keys.space = state; break
3769       case 13: this.keys.enter = state; break
3770       case 9: this.keys.tab = state; break
3771       case 8: this.keys.backspace = state; break
3772       case 16: this.keys.shift = state; break
3773       case 17: this.keys.ctrl = state; break
3774       case 18: this.keys.alt = state; break
3775     }
3776     this.keys[ev.keyCode] = state
3777   },
3778 
3779   addWindowEventListeners : function() {
3780     for (var i in this.windowEventListeners)
3781       window.addEventListener(i, this.windowEventListeners[i], false)
3782   },
3783 
3784   removeWindowEventListeners : function() {
3785     for (var i in this.windowEventListeners)
3786       window.removeEventListener(i, this.windowEventListeners[i], false)
3787   },
3788 
3789   addMouseEvent : function(x,y,mouseDown) {
3790     var a = this.allocMouseEvent()
3791     a[0] = x
3792     a[1] = y
3793     a[2] = mouseDown
3794     this.mouseEvents.push(a)
3795   },
3796 
3797   allocMouseEvent : function() {
3798     if (this.mouseEventStack.length > 0) {
3799       return this.mouseEventStack.pop()
3800     } else {
3801       return [null, null, null]
3802     }
3803   },
3804 
3805   freeMouseEvent : function(ev) {
3806     this.mouseEventStack.push(ev)
3807     if (this.mouseEventStack.length > 100)
3808       this.mouseEventStack.splice(0,this.mouseEventStack.length)
3809   },
3810 
3811   clearMouseEvents : function() {
3812     while (this.mouseEvents.length > 0)
3813       this.freeMouseEvent(this.mouseEvents.pop())
3814   },
3815 
3816   /**
3817     Start frame loop.
3818 
3819     The frame loop is an interval, where #onFrame is called every
3820     #frameDuration milliseconds.
3821     */
3822   play : function() {
3823     this.stop()
3824     this.realTime = new Date().getTime()
3825     this.frameLoop = setInterval(this.frameHandler, this.frameDuration)
3826     this.isPlaying = true
3827   },
3828 
3829   /**
3830     Stop frame loop.
3831     */
3832   stop : function() {
3833     this.__blurStop = false
3834     if (this.frameLoop) {
3835       clearInterval(this.frameLoop)
3836       this.frameLoop = false
3837     }
3838     this.isPlaying = false
3839   },
3840 
3841   dispatchEvent : function(ev) {
3842     var rv = CanvasNode.prototype.dispatchEvent.call(this, ev)
3843     if (ev.cursor) {
3844       if (this.canvas.style.cursor != ev.cursor)
3845         this.canvas.style.cursor = ev.cursor
3846     } else {
3847       if (this.canvas.style.cursor != this.cursor)
3848         this.canvas.style.cursor = this.cursor
3849     }
3850     return rv
3851   },
3852 
3853   /**
3854     The frame loop function. Called every #frameDuration milliseconds.
3855     Takes an optional external time parameter (for syncing Canvases with each
3856     other, e.g. when using a Canvas as an image.)
3857 
3858     If the time parameter is given, the second parameter is used as the frame
3859     time delta (i.e. the time elapsed since last frame.)
3860 
3861     If time or timeDelta is not given, the canvas computes its own timeDelta.
3862 
3863     @param time The external time. Optional.
3864     @param timeDelta Time since last frame in milliseconds. Optional.
3865     */
3866   onFrame : function(time, timeDelta) {
3867     var ctx = this.getContext()
3868     try {
3869       var realTime = new Date().getTime()
3870       this.currentRealElapsed = (realTime - this.realTime)
3871       this.currentRealFps = 1000 / this.currentRealElapsed
3872       var dt = this.frameDuration * this.speed
3873       if (!this.fixedTimestep)
3874         dt = this.currentRealElapsed * this.speed
3875       this.realTime = realTime
3876       if (time != null) {
3877         this.time = time
3878         if (timeDelta)
3879           dt = timeDelta
3880       } else {
3881         this.time += dt
3882       }
3883       this.previousTarget = this.target
3884       this.target = null
3885       if (this.catchMouse)
3886         this.handlePick(ctx)
3887       if (this.previousTarget != this.target) {
3888         if (this.previousTarget) {
3889           var nev = document.createEvent('MouseEvents')
3890           nev.initMouseEvent('mouseout', true, true, window,
3891             0, 0, 0, 0, 0, false, false, false, false, 0, null)
3892           nev.canvasTarget = this.previousTarget
3893           this.dispatchEvent(nev)
3894         }
3895         if (this.target) {
3896           var nev = document.createEvent('MouseEvents')
3897           nev.initMouseEvent('mouseover', true, true, window,
3898             0, 0, 0, 0, 0, false, false, false, false, 0, null)
3899           nev.canvasTarget = this.target
3900           this.dispatchEvent(nev)
3901         }
3902       }
3903       this.handleUpdate(this.time, dt)
3904       this.clearMouseEvents()
3905       if (!this.redrawOnlyWhenChanged || this.changed) {
3906         try {
3907           this.handleDraw(ctx)
3908         } catch(e) {
3909           console.log(e)
3910           throw(e)
3911         }
3912         this.changed = false
3913       }
3914       this.currentElapsed = (new Date().getTime() - this.realTime)
3915       this.elapsed += this.currentElapsed
3916       this.currentFps = 1000 / this.currentElapsed
3917       this.frame++
3918       if (this.frame % this.fpsFrames == 0) {
3919         this.fps = this.fpsFrames*1000 / (this.elapsed)
3920         this.realFps = this.fpsFrames*1000 / (new Date().getTime() - this.startTime)
3921         this.elapsed = 0
3922         this.startTime = new Date().getTime()
3923       }
3924     } catch(e) {
3925       if (ctx) {
3926         // screwed up, context is borked
3927         try {
3928           // FIXME don't be stupid
3929           for (var i=0; i<1000; i++)
3930             ctx.restore()
3931         } catch(er) {}
3932       }
3933       delete this.context
3934       throw(e)
3935     }
3936   },
3937 
3938   /**
3939     Returns the canvas drawing context object.
3940 
3941     @return Canvas drawing context
3942     */
3943   getContext : function() {
3944     if (this.recording)
3945       return this.getRecordingContext()
3946     else if (this.useMockContext)
3947       return this.getMockContext()
3948     else
3949       return this.get2DContext()
3950   },
3951 
3952   /**
3953     Gets and returns an augmented canvas 2D drawing context.
3954 
3955     The canvas 2D context is augmented by setter functions for all
3956     its instance variables, making it easier to record canvas operations in
3957     a cross-browser fashion.
3958     */
3959   get2DContext : function() {
3960     if (!this.context) {
3961       var ctx = CanvasSupport.getContext(this.canvas, '2d')
3962       this.context = ctx
3963     }
3964     return this.context
3965   },
3966 
3967   /**
3968     Creates and returns a mock drawing context.
3969 
3970     @return Mock drawing context
3971     */
3972   getMockContext : function() {
3973     if (!this.fakeContext) {
3974       var ctx = this.get2DContext()
3975       this.fakeContext = {}
3976       var f = function(){ return this }
3977       for (var i in ctx) {
3978         if (typeof(ctx[i]) == 'function')
3979           this.fakeContext[i] = f
3980         else
3981           this.fakeContext[i] = ctx[i]
3982       }
3983       this.fakeContext.isMockObject = true
3984       this.fakeContext.addColorStop = f
3985     }
3986     return this.fakeContext
3987   },
3988 
3989   getRecordingContext : function() {
3990     if (!this.recordingContext)
3991       this.recordingContext = new RecordingContext()
3992     return this.recordingContext
3993   },
3994 
3995   /**
3996     Canvas drawPickingPath uses the canvas rectangle as its path.
3997 
3998     @param ctx Canvas drawing context
3999     */
4000   drawPickingPath : function(ctx) {
4001     ctx.rect(0,0, this.canvas.width, this.canvas.height)
4002   },
4003 
4004   isPointInPath : function(x,y) {
4005     return ((x >= 0) && (x <= this.canvas.width) && (y >= 0) && (y <= this.canvas.height))
4006   },
4007 
4008   /**
4009     Sets globalAlpha to this.opacity and clears the canvas if #clear is set to
4010     true. If #fill is also set to true, fills the canvas rectangle instead of
4011     clearing (using #fillStyle as the color.)
4012 
4013     @param ctx Canvas drawing context
4014     */
4015   draw : function(ctx) {
4016     ctx.setGlobalAlpha( this.opacity )
4017     if (this.clear) {
4018       if (ctx.fillOn) {
4019         ctx.beginPath()
4020         ctx.rect(0,0, this.canvas.width, this.canvas.height)
4021         ctx.fill()
4022       } else {
4023         ctx.clearRect(0,0, this.canvas.width, this.canvas.height)
4024       }
4025     }
4026     // set default fill and stroke for the canvas contents
4027     ctx.fillStyle = 'black'
4028     ctx.strokeStyle = 'black'
4029     ctx.fillOn = false
4030     ctx.strokeOn = false
4031   }
4032 })
4033 
4034 
4035 /**
4036   Hacky link class for emulating <a>.
4037 
4038   The correct way would be to have a real <a> under the cursor while hovering
4039   this, or an imagemap polygon built from the clipped subtree path.
4040 
4041   @param href Link href.
4042   @param target Link target, defaults to _self.
4043   @param config Optional config hash.
4044   */
4045 LinkNode = Klass(CanvasNode, {
4046   href : null,
4047   target : '_self',
4048   cursor : 'pointer',
4049 
4050   initialize : function(href, target, config) {
4051     this.href = href
4052     if (target)
4053       this.target = target
4054     CanvasNode.initialize.call(this, config)
4055     this.setupLinkEventListeners()
4056   },
4057 
4058   setupLinkEventListeners : function() {
4059     this.addEventListener('click', function(ev) {
4060       if (ev.button == Mouse.RIGHT) return
4061       var target = this.target
4062       if ((ev.ctrlKey || ev.button == Mouse.MIDDLE) && target == '_self')
4063         target = '_blank'
4064       window.open(this.href, target)
4065     }, false)
4066   }
4067 })
4068 
4069 
4070 /**
4071   AudioNode is a CanvasNode used to play a sound.
4072 
4073   */
4074 AudioNode = Klass(CanvasNode, {
4075   ready : false,
4076   autoPlay : false,
4077   playing : false,
4078   paused : false,
4079   pan : 0,
4080   volume : 1,
4081   loop : false,
4082 
4083   transformSound : false,
4084 
4085   initialize : function(filename, params) {
4086     CanvasNode.initialize.call(this, params)
4087     this.filename = filename
4088     this.when('load', this._autoPlaySound)
4089     this.loadSound()
4090   },
4091 
4092   loadSound : function() {
4093     this.sound = CanvasSupport.getSoundObject()
4094     if (!this.sound) return
4095     var self = this
4096     this.sound.onready = function() {
4097       self.ready = true
4098       self.root.dispatchEvent({type: 'ready', canvasTarget: self})
4099     }
4100     this.sound.onload = function() {
4101       self.loaded = true
4102       self.root.dispatchEvent({type: 'load', canvasTarget: self})
4103     }
4104     this.sound.onerror = function() {
4105       self.root.dispatchEvent({type: 'error', canvasTarget: self})
4106     }
4107     this.sound.onfinish = function() {
4108       if (self.loop) self.play()
4109       else self.stop()
4110     }
4111     this.sound.load(this.filename)
4112   },
4113 
4114   play : function() {
4115     this.playing = true
4116     this.needPlayUpdate = true
4117   },
4118 
4119   stop : function() {
4120     this.playing = false
4121     this.needPlayUpdate = true
4122   },
4123 
4124   pause : function() {
4125     if (this.needPauseUpdate) {
4126       this.needPauseUpdate = false
4127       return
4128     }
4129     this.paused = !this.paused
4130     this.needPauseUpdate = true
4131   },
4132 
4133   setVolume : function(v) {
4134     this.volume = v
4135     this.needStatusUpdate = true
4136   },
4137 
4138   setPan : function(p) {
4139     this.pan = p
4140     this.needStatusUpdate = true
4141   },
4142 
4143   handleUpdate : function() {
4144     CanvasNode.handleUpdate.apply(this, arguments)
4145     if (this.willBeDrawn) {
4146       this.transform(null, true)
4147       if (!this.sound) this.loadSound()
4148       if (this.ready) {
4149         if (this.transformSound) {
4150           var x = this.currentMatrix[4]
4151           var y = this.currentMatrix[5]
4152           var a = this.currentMatrix[2]
4153           var b = this.currentMatrix[3]
4154           var c = this.currentMatrix[0]
4155           var d = this.currentMatrix[1]
4156           var hw = this.root.width * 0.5
4157           var ys = Math.sqrt(a*a + b*b)
4158           var xs = Math.sqrt(c*c + d*d)
4159           this.setVolume(ys)
4160           this.setPan((x - hw) / hw)
4161         }
4162         if (this.needPauseUpdate) {
4163           this.needPauseUpdate = false
4164           this._pauseSound()
4165         }
4166         if (this.needPlayUpdate) {
4167           this.needPlayUpdate = false
4168           if (this.playing) this._playSound()
4169           else this._stopSound()
4170         }
4171         if (this.needStatusUpdate) {
4172           this._setSoundVolume()
4173           this._setSoundPan()
4174         }
4175       }
4176     }
4177   },
4178 
4179   _autoPlaySound : function() {
4180     if (this.autoPlay) this.play()
4181   },
4182 
4183   _setSoundVolume : function() {
4184     this.sound.setVolume(this.volume)
4185   },
4186 
4187   _setSoundPan : function() {
4188     this.sound.setPan(this.pan)
4189   },
4190 
4191   _playSound : function() {
4192     if (this.sound.play() == false)
4193       return this.playing = false
4194     this.root.dispatchEvent({type: 'play', canvasTarget: this})
4195   },
4196 
4197   _stopSound : function() {
4198     this.sound.stop()
4199     this.root.dispatchEvent({type: 'stop', canvasTarget: this})
4200   },
4201 
4202   _pauseSound : function() {
4203     this.sound.pause()
4204     this.root.dispatchEvent({type: this.paused ? 'pause' : 'play', canvasTarget: this})
4205   }
4206 })
4207 
4208 
4209 /**
4210   ElementNode is a CanvasNode that has an HTML element as its content.
4211 
4212   The content is added to an absolutely positioned HTML element, which is added
4213   to the root node's canvases parentNode. The content element follows the
4214   current transformation matrix.
4215 
4216   The opacity of the element is set to the globalAlpha of the drawing context
4217   unless #noAlpha is true.
4218 
4219   The font-size of the element is set to the current y-scale unless #noScaling
4220   is true.
4221 
4222   Use ElementNode when you need accessible web content in your animations.
4223 
4224     var e = new ElementNode(
4225       E('h1', 'HERZLICH WILLKOMMEN IM BAHNHOF'),
4226       {
4227         x : 40,
4228         y : 30
4229       }
4230     )
4231     e.addFrameListener(function(t) {
4232       this.scale = 1 + 0.5*Math.cos(t/1000)
4233     })
4234 
4235   @param content An HTML element or string of HTML to use as the content.
4236   @param config Optional config has.
4237   */
4238 ElementNode = Klass(CanvasNode, {
4239   noScaling : false,
4240   noAlpha : false,
4241   inherit : 'inherit',
4242   align: null, // left | center | right
4243   valign: null, // top | center | bottom
4244   xOffset: 0,
4245   yOffset: 0,
4246 
4247   initialize : function(content, config) {
4248     CanvasNode.initialize.call(this, config)
4249     this.content = content
4250     this.element = E('div', content)
4251     this.element.style.MozTransformOrigin =
4252     this.element.style.webkitTransformOrigin = '0,0'
4253     this.element.style.position = 'absolute'
4254   },
4255 
4256   clone : function() {
4257     var c = CanvasNode.prototype.clone.call(this)
4258     if (this.content && this.content.cloneNode)
4259       c.content = this.content.cloneNode(true)
4260     c.element = E('div', c.content)
4261     c.element.style.position = 'absolute'
4262     c.element.style.webkitTransformOrigin = '0,0'
4263     return c
4264   },
4265 
4266   setRoot : function(root) {
4267     CanvasNode.setRoot.call(this, root)
4268     if (this.element && this.element.parentNode && this.element.parentNode.removeChild)
4269       this.element.parentNode.removeChild(this.element)
4270   },
4271 
4272   handleUpdate : function(t, dt) {
4273     CanvasNode.handleUpdate.call(this, t, dt)
4274     if (!this.willBeDrawn || !this.visible || this.display == 'none' || this.visibility == 'hidden' || !this.drawable) {
4275       if (this.element.style.display != 'none')
4276         this.element.style.display = 'none'
4277     } else if (this.element.style.display == 'none') {
4278       this.element.style.display = 'block'
4279     }
4280   },
4281 
4282   addEventListener : function(event, callback, capture) {
4283     var th = this
4284     var ccallback = function() { callback.apply(th, arguments) }
4285     return this.element.addEventListener(event, ccallback, capture||false)
4286   },
4287 
4288   removeEventListener : function(event, callback, capture) {
4289     var th = this
4290     var ccallback = function() { callback.apply(th, arguments) }
4291     return this.element.removeEventListener(event, ccallback, capture||false)
4292   },
4293 
4294   draw : function(ctx) {
4295     if (this.cursor && this.element.style.cursor != this.cursor)
4296       this.element.style.cursor = this.cursor
4297     if (this.element.style.zIndex != this.zIndex)
4298       this.element.style.zIndex = this.zIndex
4299     var baseTransform = this.currentMatrix.slice(0,4).concat([0,0])
4300     xo = this.xOffset
4301     yo = this.yOffset
4302     if (this.fillBoundingBox && this.parent && this.parent.getBoundingBox) {
4303       var bb = this.parent.getBoundingBox()
4304       xo += bb[0]
4305       yo += bb[1]
4306     }
4307     var xy = CanvasSupport.tMatrixMultiplyPoint(baseTransform,
4308       xo, yo)
4309     var x = this.currentMatrix[4] + xy[0]
4310     var y = this.currentMatrix[5] + xy[1]
4311     var a = this.currentMatrix[2]
4312     var b = this.currentMatrix[3]
4313     var c = this.currentMatrix[0]
4314     var d = this.currentMatrix[1]
4315     var ys = Math.sqrt(a*a + b*b)
4316     var xs = Math.sqrt(c*c + d*d)
4317     if (ctx.fontFamily != null)
4318       this.element.style.fontFamily = ctx.fontFamily
4319 
4320     var wkt = CanvasSupport.getSupportsCSSTransform()
4321     if (wkt && !this.noScaling) {
4322       var emt
4323       var fc = this.element.firstChild
4324       if (fc && fc.style)
4325         emt = fc.style.marginTop
4326       if (emt) {
4327         var mt = parseFloat(emt)
4328         fc.style.marginTop = ys*mt + (emt.match(/[^\d]+$/) || '')
4329       }
4330       this.element.style.MozTransform =
4331       this.element.style.webkitTransform = 'matrix('+baseTransform.join(",")+')'
4332     } else {
4333       this.element.style.MozTransform =
4334       this.element.style.webkitTransform = ''
4335     }
4336     if (ctx.fontSize != null) {
4337       if (this.noScaling || wkt) {
4338         this.element.style.fontSize = ctx.fontSize + 'px'
4339       } else {
4340         this.element.style.fontSize = ctx.fontSize * ys + 'px'
4341       }
4342     } else {
4343       if (this.noScaling || wkt) {
4344         this.element.style.fontSize = 'inherit'
4345       } else {
4346         this.element.style.fontSize = 100 * ys + '%'
4347       }
4348     }
4349     if (this.noAlpha)
4350       this.element.style.opacity = 1
4351     else
4352       this.element.style.opacity = ctx.globalAlpha
4353     if (!this.element.parentNode && this.root.canvas.parentNode) {
4354       this.element.style.visibility = 'hidden'
4355       this.root.canvas.parentNode.appendChild(this.element)
4356       var hidden = true
4357     }
4358     var fs = this.color || this.fill
4359     if (this.parent) {
4360       if (!fs || !fs.length)
4361         fs = this.parent.color
4362       if (!fs || !fs.length)
4363         fs = this.parent.fill
4364     }
4365     if (!fs || !fs.length)
4366       fs = ctx.fillStyle
4367     if (typeof(fs) == 'string') {
4368       if (fs.search(/^rgba\(/) != -1) {
4369         this.element.style.color = 'rgb(' +
4370           fs.match(/\d+/g).slice(0,3).join(",") +
4371           ')'
4372       } else {
4373         this.element.style.color = fs
4374       }
4375     } else if (fs.length) {
4376       this.element.style.color = 'rgb(' + fs.slice(0,3).map(Math.floor).join(",") + ')'
4377     }
4378     if (bb) {
4379       this.element.style.width = xs * bb[2] + 'px'
4380       this.element.style.height = ys * bb[3] + 'px'
4381       this.eWidth = xs
4382       this.eHeight = ys
4383     } else {
4384       this.element.style.width = 'default'
4385       this.element.style.height = 'default'
4386       var align = this.align || this.textAnchor
4387       if (align == 'center' || align == 'middle') {
4388         x -= this.element.offsetWidth / 2
4389       } else if (align == 'right') {
4390         x -= this.element.offsetWidth
4391       }
4392       var valign = this.valign
4393       if (valign == 'center' || valign == 'middle') {
4394         y -= this.element.offsetHeight / 2
4395       } else if (valign == 'bottom') {
4396         y -= this.element.offsetHeight
4397       }
4398       this.eWidth = this.element.offsetWidth / xs
4399       this.eHeight = this.element.offsetHeight / ys
4400     }
4401     this.element.style.left = x + 'px'
4402     this.element.style.top = y + 'px'
4403     if (hidden)
4404       this.element.style.visibility = 'visible'
4405   }
4406 })
4407 
4408 
4409 /**
4410   A Drawable is a CanvasNode with possible fill, stroke and clip.
4411 
4412   It draws the path by calling #drawGeometry
4413   */
4414 Drawable = Klass(CanvasNode, {
4415   pickable : true,
4416   //   'inside' // clip before drawing the stroke
4417   // | 'above'  // draw stroke after the fill
4418   // | 'below'  // draw stroke before the fill
4419   strokeMode : 'above',
4420 
4421   ABOVE : 'above', BELOW : 'below', INSIDE : 'inside',
4422 
4423   initialize : function(config) {
4424     CanvasNode.initialize.call(this, config)
4425   },
4426 
4427   /**
4428     Draws the picking path for the Drawable.
4429 
4430     The default version begins a new path and calls drawGeometry.
4431 
4432     @param ctx Canvas drawing context
4433     */
4434   drawPickingPath : function(ctx) {
4435     if (!this.drawGeometry) return
4436     ctx.beginPath()
4437     this.drawGeometry(ctx)
4438   },
4439 
4440   /**
4441     Returns true if the point x,y is inside the path of a drawable node.
4442 
4443     The x,y point is in user-space coordinates, meaning that e.g. the point
4444     5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the
4445     transform on the rectangle.
4446 
4447     @param x X-coordinate of the point.
4448     @param y Y-coordinate of the point.
4449     @return Whether the point is inside the path of this node.
4450     @type boolean
4451     */
4452   isPointInPath : function(x, y) {
4453     return false
4454   },
4455 
4456   isVisible : function(ctx) {
4457     var abb = this.getAxisAlignedBoundingBox()
4458     if (!abb) return true
4459     var x1 = abb[0], x2 = abb[0]+abb[2], y1 = abb[1], y2 = abb[1]+abb[3]
4460     var w = this.root.width
4461     var h = this.root.height
4462     if (this.root.drawBoundingBoxes) {
4463       ctx.save()
4464         var bbox = this.getBoundingBox()
4465         ctx.beginPath()
4466         ctx.rect(bbox[0], bbox[1], bbox[2], bbox[3])
4467         ctx.strokeStyle = 'green'
4468         ctx.lineWidth = 1
4469         ctx.stroke()
4470       ctx.restore()
4471       ctx.save()
4472         CanvasSupport.setTransform(ctx, [1,0,0,1,0,0], this.currentMatrix)
4473         ctx.beginPath()
4474         ctx.rect(x1, y1, x2-x1, y2-y1)
4475         ctx.strokeStyle = 'red'
4476         ctx.lineWidth = 1.5
4477         ctx.stroke()
4478       ctx.restore()
4479     }
4480     var visible = !(x2 < 0 || x1 > w || y2 < 0 || y1 > h)
4481     return visible
4482   },
4483 
4484   createSubtreePath : function(ctx, skipTransform) {
4485     ctx.save()
4486     if (!skipTransform) this.transform(ctx, true)
4487     if (this.drawGeometry) this.drawGeometry(ctx)
4488     for (var i=0; i<this.childNodes.length; i++)
4489       this.childNodes[i].createSubtreePath(ctx)
4490     ctx.restore()
4491   },
4492 
4493   /**
4494     Draws the Drawable. Begins a path and calls this.drawGeometry, followed by
4495     possibly filling, stroking and clipping the path, depending on whether
4496     #fill, #stroke and #clip are set.
4497 
4498     @param ctx Canvas drawing context
4499     */
4500   draw : function(ctx) {
4501     if (!this.drawGeometry) return
4502     // bbox checking is slower than just drawing in most cases.
4503     // and caching the bboxes is hard to do correctly.
4504     // plus, bboxes aren't hierarchical.
4505     // so we are being glib :|
4506     if (this.root.drawBoundingBoxes)
4507       this.isVisible(ctx)
4508     var ft = (ctx.fillStyle.transformList ||
4509               ctx.fillStyle.matrix ||
4510               ctx.fillStyle.scale != null ||
4511               ctx.fillStyle.rotation ||
4512               ctx.fillStyle.x ||
4513               ctx.fillStyle.y )
4514     var st = (ctx.strokeStyle.transformList ||
4515               ctx.strokeStyle.matrix ||
4516               ctx.strokeStyle.scale != null ||
4517               ctx.strokeStyle.rotation ||
4518               ctx.strokeStyle.x ||
4519               ctx.strokeStyle.y )
4520     ctx.beginPath()
4521     this.drawGeometry(ctx)
4522     if (ctx.strokeOn) {
4523       switch (this.strokeMode) {
4524         case this.ABOVE:
4525           if (ctx.fillOn) this.doFill(ctx,ft)
4526           this.doStroke(ctx, st)
4527           break
4528         case this.BELOW:
4529           this.doStroke(ctx, st)
4530           if (ctx.fillOn) this.doFill(ctx,