From 9354f69ddac5b9e1c1b3d8e47b4367540606efd2 Mon Sep 17 00:00:00 2001 From: dsc Date: Sat, 30 Oct 2010 16:54:53 -0700 Subject: [PATCH] Initial commit --- css/log.css | 12 + css/lttl.css | 18 + css/reset.css | 2 + css/test.css | 4 + index.php | 67 + lib/cake.js | 7657 ++++++++++++++++++++++ lib/excanvas.js | 924 +++ lib/excanvas.min.js | 35 + lib/functional.js | 1050 +++ lib/gury.js | 391 ++ lib/jquery-1.4.3.js | 6883 ++++++++++++++++++++ lib/jquery-1.4.3.min.js | 166 + lib/jquery.hotkeys.js | 99 + lib/jquery.hotkeys.min.js | 1 + lib/processing-0.9.7.js |13095 ++++++++++++++++++++++++++++++++++++++ lib/processing-0.9.7.min.js | 1 + lib/qunit/qunit.css | 155 + lib/qunit/qunit.js | 1261 ++++ lib/raphael-min.js | 7 + lib/raphael.js | 3725 +++++++++++ lib/uki-0.3.8.js | 8022 +++++++++++++++++++++++ lib/uki-0.3.8.min.js | 232 + lib/uki-more-0.3.8.min.js | 39 + src/Y/_intro.js | 4 + src/Y/_outro.js | 2 + src/Y/alias.js | 13 + src/Y/core.js | 40 + src/Y/modules/event.js | 68 + src/Y/modules/metaclass.js | 322 + src/Y/modules/y.event.js | 101 + src/Y/modules/y.json.js | 52 + src/Y/modules/y.kv.js | 26 + src/Y/modules/y.op.js | 100 + src/Y/modules/y.plugin-arch.js | 170 + src/Y/modules/y.polyevent.js | 203 + src/Y/type.js | 52 + src/Y/y-array.js | 74 + src/Y/y-class.js | 157 + src/Y/y-collection.js | 141 + src/Y/y-core.js | 170 + src/Y/y-function.js | 204 + src/Y/y-number.js | 40 + src/Y/y-object.js | 12 + src/Y/y-op.js | 59 + src/Y/y-string.js | 66 + src/Y/y.js.php | 51 + src/js/_intro.js | 7 + src/js/_outro.js | 6 + src/js/core.js | 39 + src/js/function.js | 46 + src/js/js.js.php | 46 + src/js/type.js | 49 + src/lessly/bitgrid.js | 381 ++ src/lessly/draw.js | 44 + src/lessly/future.js | 107 + src/lessly/log.js | 64 + src/lessly/log.uki.js | 4 + src/lessly/viewport.js | 60 + src/portal/layer.js | 274 + src/portal/shape.js | 61 + src/portal/simpleclass.js | 34 + src/portal/util/cooldown.js | 41 + src/portal/util/eventloop.js | 106 + src/portal/util/loc.js | 113 + src/portal/util/pointquadtree.js | 580 ++ src/portal/util/quadtree.js | 205 + src/portal/util/rbtree.js | 467 ++ src/simoon/ability/ability.js | 61 + src/simoon/ability/laser.js | 75 + src/simoon/ability/projectile.js | 28 + src/simoon/game/calc.js | 24 + src/simoon/game/draw.js | 73 + src/simoon/game/game.js | 54 + src/simoon/game/map.js | 89 + src/simoon/globals.js | 32 + src/simoon/grid/grid.js | 52 + src/simoon/player/test-player.js | 48 + src/simoon/simoon.js | 16 + src/simoon/ui.js | 99 + src/simoon/unit/agent.js | 243 + src/simoon/unit/creep.js | 47 + src/simoon/unit/tower.js | 43 + src/simoon/unit/unit.js | 26 + src/tanks/lttl.js | 15 + src/tanks/tank.js | 2 + src/u.js | 88 + 86 files changed, 49822 insertions(+), 0 deletions(-) create mode 100644 css/log.css create mode 100644 css/lttl.css create mode 100644 css/reset.css create mode 100644 css/test.css create mode 100644 index.php create mode 100644 lib/cake.js create mode 100644 lib/excanvas.js create mode 100644 lib/excanvas.min.js create mode 100644 lib/functional.js create mode 100644 lib/gury.js create mode 100644 lib/jquery-1.4.3.js create mode 100644 lib/jquery-1.4.3.min.js create mode 100644 lib/jquery.hotkeys.js create mode 100644 lib/jquery.hotkeys.min.js create mode 100644 lib/processing-0.9.7.js create mode 100644 lib/processing-0.9.7.min.js create mode 100644 lib/qunit/qunit.css create mode 100644 lib/qunit/qunit.js create mode 100644 lib/raphael-min.js create mode 100644 lib/raphael.js create mode 100644 lib/uki-0.3.8.js create mode 100644 lib/uki-0.3.8.min.js create mode 100644 lib/uki-more-0.3.8.min.js create mode 100644 notes.md create mode 100644 src/Y/_intro.js create mode 100644 src/Y/_outro.js create mode 100644 src/Y/alias.js create mode 100644 src/Y/core.js create mode 100644 src/Y/modules/event.js create mode 100644 src/Y/modules/metaclass.js create mode 100644 src/Y/modules/y.event.js create mode 100644 src/Y/modules/y.json.js create mode 100644 src/Y/modules/y.kv.js create mode 100644 src/Y/modules/y.op.js create mode 100644 src/Y/modules/y.plugin-arch.js create mode 100644 src/Y/modules/y.polyevent.js create mode 100644 src/Y/type.js create mode 100644 src/Y/y-array.js create mode 100644 src/Y/y-class.js create mode 100644 src/Y/y-collection.js create mode 100644 src/Y/y-core.js create mode 100644 src/Y/y-function.js create mode 100644 src/Y/y-number.js create mode 100644 src/Y/y-object.js create mode 100644 src/Y/y-op.js create mode 100644 src/Y/y-string.js create mode 100644 src/Y/y.js.php create mode 100644 src/js/_intro.js create mode 100644 src/js/_outro.js create mode 100644 src/js/array.js create mode 100644 src/js/core.js create mode 100644 src/js/function.js create mode 100644 src/js/js.js.php create mode 100644 src/js/number.js create mode 100644 src/js/object.js create mode 100644 src/js/regexp.js create mode 100644 src/js/string.js create mode 100644 src/js/type.js create mode 100644 src/lessly/bitgrid.js create mode 100644 src/lessly/draw.js create mode 100644 src/lessly/future.js create mode 100644 src/lessly/log.js create mode 100644 src/lessly/log.uki.js create mode 100644 src/lessly/viewport.js create mode 100644 src/portal/layer.js create mode 100644 src/portal/path.js create mode 100644 src/portal/portal.js create mode 100644 src/portal/shape.js create mode 100644 src/portal/simpleclass.js create mode 100644 src/portal/util/cooldown.js create mode 100644 src/portal/util/eventloop.js create mode 100644 src/portal/util/loc.js create mode 100644 src/portal/util/pointquadtree.js create mode 100644 src/portal/util/quadtree.js create mode 100644 src/portal/util/rbtree.js create mode 100644 src/simoon/ability/ability.js create mode 100644 src/simoon/ability/laser.js create mode 100644 src/simoon/ability/projectile.js create mode 100644 src/simoon/game/calc.js create mode 100644 src/simoon/game/draw.js create mode 100644 src/simoon/game/game.js create mode 100644 src/simoon/game/map.js create mode 100644 src/simoon/globals.js create mode 100644 src/simoon/grid/grid.js create mode 100644 src/simoon/player/player.js create mode 100644 src/simoon/player/test-player.js create mode 100644 src/simoon/simoon.js create mode 100644 src/simoon/ui.js create mode 100644 src/simoon/unit/agent.js create mode 100644 src/simoon/unit/creep.js create mode 100644 src/simoon/unit/tower.js create mode 100644 src/simoon/unit/unit.js create mode 100644 src/tanks/game.js create mode 100644 src/tanks/lttl.js create mode 100644 src/tanks/map.js create mode 100644 src/tanks/tank.js create mode 100644 src/tanks/ui.js create mode 100644 src/u.js diff --git a/css/log.css b/css/log.css new file mode 100644 index 0000000..897fee1 --- /dev/null +++ b/css/log.css @@ -0,0 +1,12 @@ +#log { + position:fixed; top:0; right:0; width:50%; height:100%; overflow:hidden; + border-left:1px solid #bbb; background-color: #fff; color:#333; } +#log h5 { font-size:1.25em; font-weight:normal; text-transform:uppercase; letter-spacing:0.16667em; } + +.log_meta { position:absolute; top:0; left:0; width:100%; z-index:2; background-color:#fff; } +.log_meta > * { padding:0.3em; } + .log_menu { color:#fff; background-color:#bbb; } + .log_menu a { color:#fff; cursor:pointer; } +.log_container { position:relative; width:100%; height:100%; overflow:auto; z-index:1; } + .log_item { padding:0.5em; font:normal normal 10pt/1 Menlo, Monospace; text-decoration:none; } + diff --git a/css/lttl.css b/css/lttl.css new file mode 100644 index 0000000..d746829 --- /dev/null +++ b/css/lttl.css @@ -0,0 +1,18 @@ +html, body { width:100%; height:100%; + font-family:Geogrotesque,Helvetica; color:#fff; background-color:#3F3F3F; } +body { font-family:Geogrotesque, Helvetica; font-size:12pt; } +h1 { position:fixed; top:0; right:0; margin:0; padding:0; font-size:3em; color:#000; opacity:0.25; z-index:100; } +ul, ol, li { list-style: none ! important; margin:0; padding:0; } + +.rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; } + +#viewport { position:relative; top:1em; width:500px; height:500px; margin:0 auto; } + +#howto { position:fixed; top:3em; right:1em; color:#BFBFBF; } + +#info { position:fixed; bottom:10px; right:10px; padding:0.5em; background-color:rgba(0,0,0, 0.1); color:#787878; } + #info label { display:block; float:left; width:3em; margin-right:0.5em; color:#787878; } + #info input { border:0; background-color:transparent; min-width:5em; width:5em; color:#5c5c5c; } + +#log { position:fixed; top:auto; bottom:0; left:0; width:100%; height:30%; border-top:1px solid #bbb; } + diff --git a/css/reset.css b/css/reset.css new file mode 100644 index 0000000..dd5886b --- /dev/null +++ b/css/reset.css @@ -0,0 +1,2 @@ +html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;font-variant:normal;}sup {vertical-align:text-top;}sub {vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;} +h1{font-size:138.5%;}h2{font-size:123.1%;}h3{font-size:108%;}h1,h2,h3{margin:1em 0;}h1,h2,h3,h4,h5,h6,strong{font-weight:bold;}abbr,acronym{border-bottom:1px dotted #000;cursor:help;} em{font-style:italic;}blockquote,ul,ol,dl{margin:1em;}ol,ul,dl{margin-left:2em;}ol li{list-style:decimal outside;}ul li{list-style:disc outside;}dl dd{margin-left:1em;}th,td{padding:.5em;}th{font-weight:bold;text-align:center;}caption{margin-bottom:.5em;text-align:center;}p,fieldset,table,pre{margin-bottom:1em;}input[type=text],input[type=password],textarea{width:12.25em;*width:11.9em;} \ No newline at end of file diff --git a/css/test.css b/css/test.css new file mode 100644 index 0000000..1c2d3e4 --- /dev/null +++ b/css/test.css @@ -0,0 +1,4 @@ +html, body { width:100%; height:100%; + font-family:Geogrotesque,Helvetica; color:#fff; background-color:#3F3F3F; } +h1:not([id]) { position:fixed; top:-0.25em; right:-0.25em; margin:0; padding:0; + font-size:3em; color:#000; opacity:0.25; z-index:100; } diff --git a/index.php b/index.php new file mode 100644 index 0000000..8806461 --- /dev/null +++ b/index.php @@ -0,0 +1,67 @@ + + + +The Littlest Battletank + + + + + + +
+ +

The Littlest Battletank

+ + + + + +
+ +\n"; +} + +foreach ($scripts as $s) js($s); +?> +
+ + + \ No newline at end of file diff --git a/lib/cake.js b/lib/cake.js new file mode 100644 index 0000000..e222e3c --- /dev/null +++ b/lib/cake.js @@ -0,0 +1,7657 @@ +/* +CAKE - Canvas Animation Kit Experiment + +Copyright (C) 2007 Ilmari Heikkinen + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + + +/** + Delete the first instance of obj from the array. + + @param obj The object to delete + @return true on success, false if array contains no instances of obj + @type boolean + @addon + */ +Array.prototype.deleteFirst = function(obj) { + for (var i=0; i 'window' + var g = f.bind(obj) + g() + // => 'obj' + + @param object Object to bind this function to + @return Function bound to object + @addon + */ + Function.prototype.bind = function(object) { + var t = this + return function() { + return t.apply(object, arguments) + } + } +} + +if (!Array.prototype.last) { + /** + Returns the last element of the array. + + @return The last element of the array + @addon + */ + Array.prototype.last = function() { + return this[this.length-1] + } +} +if (!Array.prototype.indexOf) { + /** + Returns the index of obj if it is in the array. + Returns -1 otherwise. + + @param obj The object to find from the array. + @return The index of obj or -1 if obj isn't in the array. + @addon + */ + Array.prototype.indexOf = function(obj) { + for (var i=0; i= 0); + } +} +/** + Iterate function f over each element of the array and return an array + of the return values. + + @param f Function to apply to each element + @return An array of return values from applying f on each element of the array + @type Array + @addon + */ +Array.prototype.map = function(f) { + var na = new Array(this.length) + if (f) + for (var i=0; i 25 + + @return Constructor object for the class + */ +Klass = function() { + var c = function() { + this.initialize.apply(this, arguments) + } + c.ancestors = $A(arguments) + c.prototype = {} + for(var i = 0; i Math.PI) d -= pi2 + if (d < -Math.PI) d += pi2 + return d + }, + + linePoint : function(a, b, t) { + return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t] + }, + + quadraticPoint : function(a, b, c, t) { + // var d = this.linePoint(a,b,t) + // var e = this.linePoint(b,c,t) + // return this.linePoint(d,e,t) + var dx = a[0]+(b[0]-a[0])*t + var ex = b[0]+(c[0]-b[0])*t + var x = dx+(ex-dx)*t + var dy = a[1]+(b[1]-a[1])*t + var ey = b[1]+(c[1]-b[1])*t + var y = dy+(ey-dy)*t + return [x,y] + }, + + cubicPoint : function(a, b, c, d, t) { + var ax3 = a[0]*3 + var bx3 = b[0]*3 + var cx3 = c[0]*3 + var ay3 = a[1]*3 + var by3 = b[1]*3 + var cy3 = c[1]*3 + return [ + a[0] + t*(bx3 - ax3 + t*(ax3-2*bx3+cx3 + t*(bx3-a[0]-cx3+d[0]))), + a[1] + t*(by3 - ay3 + t*(ay3-2*by3+cy3 + t*(by3-a[1]-cy3+d[1]))) + ] + }, + + linearValue : function(a,b,t) { + return a + (b-a)*t + }, + + quadraticValue : function(a,b,c,t) { + var d = a + (b-a)*t + var e = b + (c-b)*t + return d + (e-d)*t + }, + + cubicValue : function(a,b,c,d,t) { + var a3 = a*3, b3 = b*3, c3 = c*3 + return a + t*(b3 - a3 + t*(a3-2*b3+c3 + t*(b3-a-c3+d))) + }, + + catmullRomPoint : function (a,b,c,d, t) { + var af = ((-t+2)*t-1)*t*0.5 + var bf = (((3*t-5)*t)*t+2)*0.5 + var cf = ((-3*t+4)*t+1)*t*0.5 + var df = ((t-1)*t*t)*0.5 + return [ + a[0]*af + b[0]*bf + c[0]*cf + d[0]*df, + a[1]*af + b[1]*bf + c[1]*cf + d[1]*df + ] + }, + + catmullRomAngle : function (a,b,c,d, t) { + var dx = 0.5 * (c[0] - a[0] + 2*t*(2*a[0] - 5*b[0] + 4*c[0] - d[0]) + + 3*t*t*(3*b[0] + d[0] - a[0] - 3*c[0])) + var dy = 0.5 * (c[1] - a[1] + 2*t*(2*a[1] - 5*b[1] + 4*c[1] - d[1]) + + 3*t*t*(3*b[1] + d[1] - a[1] - 3*c[1])) + return Math.atan2(dy, dx) + }, + + catmullRomPointAngle : function (a,b,c,d, t) { + var p = this.catmullRomPoint(a,b,c,d,t) + var a = this.catmullRomAngle(a,b,c,d,t) + return {point:p, angle:a} + }, + + lineAngle : function(a,b) { + return Math.atan2(b[1]-a[1], b[0]-a[0]) + }, + + quadraticAngle : function(a,b,c,t) { + var d = this.linePoint(a,b,t) + var e = this.linePoint(b,c,t) + return this.lineAngle(d,e) + }, + + cubicAngle : function(a, b, c, d, t) { + var e = this.quadraticPoint(a,b,c,t) + var f = this.quadraticPoint(b,c,d,t) + return this.lineAngle(e,f) + }, + + lineLength : function(a,b) { + var x = (b[0]-a[0]) + var y = (b[1]-a[1]) + return Math.sqrt(x*x + y*y) + }, + + squareLineLength : function(a,b) { + var x = (b[0]-a[0]) + var y = (b[1]-a[1]) + return x*x + y*y + }, + + quadraticLength : function(a,b,c, error) { + var p1 = this.linePoint(a,b,2/3) + var p2 = this.linePoint(b,c,1/3) + return this.cubicLength(a,p1,p2,c, error) + }, + + cubicLength : (function() { + var bezsplit = function(v) { + var vtemp = [v.slice(0)] + + for (var i=1; i < 4; i++) { + vtemp[i] = [[],[],[],[]] + for (var j=0; j < 4-i; j++) { + vtemp[i][j][0] = 0.5 * (vtemp[i-1][j][0] + vtemp[i-1][j+1][0]) + vtemp[i][j][1] = 0.5 * (vtemp[i-1][j][1] + vtemp[i-1][j+1][1]) + } + } + var left = [] + var right = [] + for (var j=0; j<4; j++) { + left[j] = vtemp[j][0] + right[j] = vtemp[3-j][j] + } + return [left, right] + } + + var addifclose = function(v, error) { + var len = 0 + for (var i=0; i < 3; i++) { + len += Curves.lineLength(v[i], v[i+1]) + } + var chord = Curves.lineLength(v[0], v[3]) + if ((len - chord) > error) { + var lr = bezsplit(v) + len = addifclose(lr[0], error) + addifclose(lr[1], error) + } + return len + } + + return function(a,b,c,d, error) { + if (!error) error = 1 + return addifclose([a,b,c,d], error) + } + })(), + + quadraticLengthPointAngle : function(a,b,c,lt,error) { + var p1 = this.linePoint(a,b,2/3) + var p2 = this.linePoint(b,c,1/3) + return this.cubicLengthPointAngle(a,p1,p2,c, error) + }, + + cubicLengthPointAngle : function(a,b,c,d,lt,error) { + // this thing outright rapes the GC. + // how about not creating a billion arrays, hmm? + var len = this.cubicLength(a,b,c,d,error) + var point = a + var prevpoint = a + var lengths = [] + var prevlensum = 0 + var lensum = 0 + var tl = lt*len + var segs = 20 + var fac = 1/segs + for (var i=1; i<=segs; i++) { // FIXME get smarter + prevpoint = point + point = this.cubicPoint(a,b,c,d, fac*i) + prevlensum = lensum + lensum += this.lineLength(prevpoint, point) + if (lensum >= tl) { + if (lensum == prevlensum) + return {point: point, angle: this.lineAngle(a,b)} + var dl = lensum - tl + var dt = dl / (lensum-prevlensum) + return {point: this.linePoint(prevpoint, point, 1-dt), + angle: this.cubicAngle(a,b,c,d, fac*(i-dt)) } + } + } + return {point: d.slice(0), angle: this.lineAngle(c,d)} + } + +} + + + +/** + Color helper functions. + */ +Colors = { + + /** + Converts an HSL color to its corresponding RGB color. + + @param h Hue in degrees (0 .. 359) + @param s Saturation (0.0 .. 1.0) + @param l Lightness (0 .. 255) + @return The corresponding RGB color as [r,g,b] + @type Array + */ + hsl2rgb : function(h,s,l) { + var r,g,b + if (s == 0) { + r=g=b=v + } else { + var q = (l < 0.5 ? l * (1+s) : l+s-(l*s)) + var p = 2 * l - q + var hk = (h % 360) / 360 + var tr = hk + 1/3 + var tg = hk + var tb = hk - 1/3 + if (tr < 0) tr++ + if (tr > 1) tr-- + if (tg < 0) tg++ + if (tg > 1) tg-- + if (tb < 0) tb++ + if (tb > 1) tb-- + if (tr < 1/6) + r = p + ((q-p)*6*tr) + else if (tr < 1/2) + r = q + else if (tr < 2/3) + r = p + ((q-p)*6*(2/3 - tr)) + else + r = p + + if (tg < 1/6) + g = p + ((q-p)*6*tg) + else if (tg < 1/2) + g = q + else if (tg < 2/3) + g = p + ((q-p)*6*(2/3 - tg)) + else + g = p + + if (tb < 1/6) + b = p + ((q-p)*6*tb) + else if (tb < 1/2) + b = q + else if (tb < 2/3) + b = p + ((q-p)*6*(2/3 - tb)) + else + b = p + } + + return [r,g,b] + }, + + /** + Converts an HSV color to its corresponding RGB color. + + @param h Hue in degrees (0 .. 359) + @param s Saturation (0.0 .. 1.0) + @param v Value (0 .. 255) + @return The corresponding RGB color as [r,g,b] + @type Array + */ + hsv2rgb : function(h,s,v) { + var r,g,b + if (s == 0) { + r=g=b=v + } else { + h = (h % 360)/60.0 + var i = Math.floor(h) + var f = h-i + var p = v * (1-s) + var q = v * (1-s*f) + var t = v * (1-s*(1-f)) + switch (i) { + case 0: + r = v + g = t + b = p + break + case 1: + r = q + g = v + b = p + break + case 2: + r = p + g = v + b = t + break + case 3: + r = p + g = q + b = v + break + case 4: + r = t + g = p + b = v + break + case 5: + r = v + g = p + b = q + break + } + } + return [r,g,b] + }, + + /** + Parses a color style object into one that can be used with the given + canvas context. + + Accepted formats: + 'white' + '#fff' + '#ffffff' + 'rgba(255,255,255, 1.0)' + [255, 255, 255] + [255, 255, 255, 1.0] + new Gradient(...) + new Pattern(...) + + @param style The color style to parse + @param ctx Canvas 2D context on which the style is to be used + @return A parsed style, ready to be used as ctx.fillStyle / strokeStyle + */ + parseColorStyle : function(style, ctx) { + if (typeof style == 'string') { + return style + } else if (style.compiled) { + return style.compiled + } else if (style.isPattern) { + return style.compile(ctx) + } else if (style.length == 3) { + return 'rgba('+style.map(Math.round).join(",")+', 1)' + } else if (style.length == 4) { + return 'rgba('+ + Math.round(style[0])+','+ + Math.round(style[1])+','+ + Math.round(style[2])+','+ + style[3]+ + ')' + } else { // wtf + throw( "Bad style: " + style ) + } + } +} + + + +/** + Navigating around differing implementations of canvas features. + + Current issues: + + isPointInPath(x,y): + + Opera supports isPointInPath. + + Safari doesn't have isPointInPath. So you need to keep track of the CTM and + do your own in-fill-checking. Which is done for circles and rectangles + in Circle#isPointInPath and Rectangle#isPointInPath. + Paths use an inaccurate bounding box test, implemented in + Path#isPointInPath. + + Firefox 3 has isPointInPath. But it uses user-space coordinates. + Which can be easily navigated around because it has setTransform. + + Firefox 2 has isPointInPath. But it uses user-space coordinates. + And there's no setTransform, so you need to keep track of the CTM and + multiply the mouse vector with the CTM's inverse. + + Drawing text: + + Rhino has ctx.drawString(x,y, text) + + Firefox has ctx.mozDrawText(text) + + The WhatWG spec, Safari and Opera have nothing. + +*/ +CanvasSupport = { + DEVICE_SPACE : 0, // Opera + USER_SPACE : 1, // Fx2, Fx3 + isPointInPathMode : null, + supportsIsPointInPath : null, + supportsCSSTransform : null, + supportsCanvas : null, + + isCanvasSupported : function() { + if (this.supportsCanvas == null) { + var e = {}; + try { e = E('canvas'); } catch(x) {} + this.supportsCanvas = (e.getContext != null); + } + return this.supportsCanvas; + }, + + isCSSTransformSupported : function() { + if (this.supportsCSSTransform == null) { + var e = E('div') + var dbs = e.style + var s = (dbs.webkitTransform != null || dbs.MozTransform != null) + this.supportsCSSTransform = (s != null) + } + return this.supportsCSSTransform + }, + + getTestContext : function() { + if (!this.testContext) { + var c = E.canvas(1,1) + this.testContext = c.getContext('2d') + } + return this.testContext + }, + + getSupportsAudioTag : function() { + var e = E('audio') + return !!e.play + }, + + getSupportsSoundManager : function() { + return (window.soundManager && soundManager.enabled) + }, + + soundId : 0, + + getSoundObject : function() { + var e = null +// if (this.getSupportsAudioTag()) { +// e = this.getAudioTagSoundObject() +// } else + if (this.getSupportsSoundManager()) { + e = this.getSoundManagerSoundObject() + } + return e + }, + + getAudioTagSoundObject : function() { + var sid = 'sound-' + this.soundId++ + var e = E('audio', {id: sid}) + e.load = function(src) { + this.src = src + } + e.addEventListener('canplaythrough', function() { + if (this.onready) this.onready() + }, false) + e.setVolume = function(v){ this.volume = v } + e.setPan = function(v){ this.pan = v } + return e + }, + + getSoundManagerSoundObject : function() { + var sid = 'sound-' + this.soundId++ + var e = { + volume: 100, + pan: 0, + sid : sid, + load : function(src) { + return soundManager.load(this.sid, { + url: src, + autoPlay: false, + volume: this.volume, + pan: this.pan + }) + }, + _onload : function() { + if (this.onload) this.onload() + if (this.onready) this.onready() + }, + _onerror : function() { + if (this.onerror) this.onerror() + }, + _onfinish : function() { + if (this.onfinish) this.onfinish() + }, + play : function() { + return soundManager.play(this.sid) + }, + stop : function() { + return soundManager.stop(this.sid) + }, + pause : function() { + return soundManager.togglePause(this.sid) + }, + setVolume : function(v) { + this.volume = v*100 + return soundManager.setVolume(this.sid, v*100) + }, + setPan : function(v) { + this.pan = v*100 + return soundManager.setPan(this.sid, v*100) + } + } + soundManager.createSound(sid, 'null.mp3') + e.sound = soundManager.getSoundById(sid) + e.sound.options.onfinish = e._onfinish.bind(e) + e.sound.options.onload = e._onload.bind(e) + e.sound.options.onerror = e._onerror.bind(e) + return e + }, + + /** + Canvas context augment module that adds setters. + */ + ContextSetterAugment : { + setFillStyle : function(fs) { this.fillStyle = fs }, + setStrokeStyle : function(ss) { this.strokeStyle = ss }, + setGlobalAlpha : function(ga) { this.globalAlpha = ga }, + setLineWidth : function(lw) { this.lineWidth = lw }, + setLineCap : function(lw) { this.lineCap = lw }, + setLineJoin : function(lw) { this.lineJoin = lw }, + setMiterLimit : function(lw) { this.miterLimit = lw }, + setGlobalCompositeOperation : function(lw) { + this.globalCompositeOperation = lw + }, + setShadowColor : function(x) { this.shadowColor = x }, + setShadowBlur : function(x) { this.shadowBlur = x }, + setShadowOffsetX : function(x) { this.shadowOffsetX = x }, + setShadowOffsetY : function(x) { this.shadowOffsetY = x }, + setMozTextStyle : function(x) { this.mozTextStyle = x }, + setFont : function(x) { this.font = x }, + setTextAlign : function(x) { this.textAlign = x }, + setTextBaseline : function(x) { this.textBaseline = x } + }, + + ContextJSImplAugment : { + identity : function() { + CanvasSupport.setTransform(this, [1,0,0,1,0,0]) + } + }, + + /** + Augments a canvas context with setters. + */ + augment : function(ctx) { + Object.conditionalExtend(ctx, this.ContextSetterAugment) + Object.conditionalExtend(ctx, this.ContextJSImplAugment) + return ctx + }, + + /** + Gets the augmented context for canvas. + */ + getContext : function(canvas, type) { + var ctx = canvas.getContext(type || '2d') + this.augment(ctx) + return ctx + }, + + + /** + Multiplies two 3x2 affine 2D column-major transformation matrices with + each other and stores the result in the first matrix. + + Returns the multiplied matrix m1. + */ + tMatrixMultiply : function(m1, m2) { + var m11 = m1[0]*m2[0] + m1[2]*m2[1] + var m12 = m1[1]*m2[0] + m1[3]*m2[1] + + var m21 = m1[0]*m2[2] + m1[2]*m2[3] + var m22 = m1[1]*m2[2] + m1[3]*m2[3] + + var dx = m1[0]*m2[4] + m1[2]*m2[5] + m1[4] + var dy = m1[1]*m2[4] + m1[3]*m2[5] + m1[5] + + m1[0] = m11 + m1[1] = m12 + m1[2] = m21 + m1[3] = m22 + m1[4] = dx + m1[5] = dy + + return m1 + }, + + /** + Multiplies the vector [x, y, 1] with the 3x2 transformation matrix m. + */ + tMatrixMultiplyPoint : function(m, x, y) { + return [ + x*m[0] + y*m[2] + m[4], + x*m[1] + y*m[3] + m[5] + ] + }, + + /** + Inverts a 3x2 affine 2D column-major transformation matrix. + + Returns an inverted copy of the matrix. + */ + tInvertMatrix : function(m) { + var d = 1 / (m[0]*m[3]-m[1]*m[2]) + return [ + m[3]*d, -m[1]*d, + -m[2]*d, m[0]*d, + d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) + ] + }, + + /** + Applies a transformation matrix m on the canvas context ctx. + */ + transform : function(ctx, m) { + if (ctx.transform) + return ctx.transform.apply(ctx, m) + ctx.translate(m[4], m[5]) + // scale + if (Math.abs(m[1]) < 1e-6 && Math.abs(m[2]) < 1e-6) { + ctx.scale(m[0], m[3]) + return + } + var res = this.svdTransform({xx:m[0], xy:m[2], yx:m[1], yy:m[3], dx:m[4], dy:m[5]}) + ctx.rotate(res.angle2) + ctx.scale(res.sx, res.sy) + ctx.rotate(res.angle1) + return + }, + + // broken svd... + brokenSvd : function(m) { + var mt = [m[0], m[2], m[1], m[3], 0,0] + var mtm = [ + mt[0]*m[0]+mt[2]*m[1], + mt[1]*m[0]+mt[3]*m[1], + mt[0]*m[2]+mt[2]*m[3], + mt[1]*m[2]+mt[3]*m[3], + 0,0 + ] + // (mtm[0]-x) * (mtm[3]-x) - (mtm[1]*mtm[2]) = 0 + // x*x - (mtm[0]+mtm[3])*x - (mtm[1]*mtm[2])+(mtm[0]*mtm[3]) = 0 + var a = 1 + var b = -(mtm[0]+mtm[3]) + var c = -(mtm[1]*mtm[2])+(mtm[0]*mtm[3]) + var d = Math.sqrt(b*b - 4*a*c) + var c1 = (-b + d) / (2*a) + var c2 = (-b - d) / (2*a) + if (c1 < c2) + var tmp = c1, c1 = c2, c2 = tmp + var s1 = Math.sqrt(c1) + var s2 = Math.sqrt(c2) + var i_s = [1/s1, 0, 0, 1/s2, 0,0] + // (mtm[0]-c1)*x1 + mtm[2]*x2 = 0 + // mtm[1]*x1 + (mtm[3]-c1)*x2 = 0 + // x2 = -(mtm[0]-c1)*x1 / mtm[2] + var e = ((mtm[0]-c1)/mtm[2]) + var l = Math.sqrt(1 + e*e) + var v00 = 1 / l + var v10 = e / l + var v11 = v00 + var v01 = -v10 + var v = [v00, v01, v10, v11, 0,0] + var u = m.slice(0) + this.tMatrixMultiply(u,v) + this.tMatrixMultiply(u,i_s) + return [u, [s1,0,0,s2,0,0], [v00, v10, v01, v11, 0, 0]] + }, + + + svdTransform : (function(){ + // Copyright (c) 2004-2005, The Dojo Foundation + // All Rights Reserved + var m = {} + m.Matrix2D = function(arg){ + // summary: a 2D matrix object + // description: Normalizes a 2D matrix-like object. If arrays is passed, + // all objects of the array are normalized and multiplied sequentially. + // arg: Object + // a 2D matrix-like object, a number, or an array of such objects + if(arg){ + if(typeof arg == "number"){ + this.xx = this.yy = arg; + }else if(arg instanceof Array){ + if(arg.length > 0){ + var matrix = m.normalize(arg[0]); + // combine matrices + for(var i = 1; i < arg.length; ++i){ + var l = matrix, r = m.normalize(arg[i]); + matrix = new m.Matrix2D(); + matrix.xx = l.xx * r.xx + l.xy * r.yx; + matrix.xy = l.xx * r.xy + l.xy * r.yy; + matrix.yx = l.yx * r.xx + l.yy * r.yx; + matrix.yy = l.yx * r.xy + l.yy * r.yy; + matrix.dx = l.xx * r.dx + l.xy * r.dy + l.dx; + matrix.dy = l.yx * r.dx + l.yy * r.dy + l.dy; + } + Object.extend(this, matrix); + } + }else{ + Object.extend(this, arg); + } + } + } + // ensure matrix 2D conformance + m.normalize = function(matrix){ + // summary: converts an object to a matrix, if necessary + // description: Converts any 2D matrix-like object or an array of + // such objects to a valid dojox.gfx.matrix.Matrix2D object. + // matrix: Object: an object, which is converted to a matrix, if necessary + return (matrix instanceof m.Matrix2D) ? matrix : new m.Matrix2D(matrix); // dojox.gfx.matrix.Matrix2D + } + m.multiply = function(matrix){ + // summary: combines matrices by multiplying them sequentially in the given order + // matrix: dojox.gfx.matrix.Matrix2D...: a 2D matrix-like object, + // all subsequent arguments are matrix-like objects too + var M = m.normalize(matrix); + // combine matrices + for(var i = 1; i < arguments.length; ++i){ + var l = M, r = m.normalize(arguments[i]); + M = new m.Matrix2D(); + M.xx = l.xx * r.xx + l.xy * r.yx; + M.xy = l.xx * r.xy + l.xy * r.yy; + M.yx = l.yx * r.xx + l.yy * r.yx; + M.yy = l.yx * r.xy + l.yy * r.yy; + M.dx = l.xx * r.dx + l.xy * r.dy + l.dx; + M.dy = l.yx * r.dx + l.yy * r.dy + l.dy; + } + return M; // dojox.gfx.matrix.Matrix2D + } + m.invert = function(matrix) { + var M = m.normalize(matrix), + D = M.xx * M.yy - M.xy * M.yx, + M = new m.Matrix2D({ + xx: M.yy/D, xy: -M.xy/D, + yx: -M.yx/D, yy: M.xx/D, + dx: (M.xy * M.dy - M.yy * M.dx) / D, + dy: (M.yx * M.dx - M.xx * M.dy) / D + }); + return M; // dojox.gfx.matrix.Matrix2D + } + // the default (identity) matrix, which is used to fill in missing values + Object.extend(m.Matrix2D, {xx: 1, xy: 0, yx: 0, yy: 1, dx: 0, dy: 0}); + + var eq = function(/* Number */ a, /* Number */ b){ + // summary: compare two FP numbers for equality + return Math.abs(a - b) <= 1e-6 * (Math.abs(a) + Math.abs(b)); // Boolean + }; + + var calcFromValues = function(/* Number */ s1, /* Number */ s2){ + // summary: uses two close FP values to approximate the result + if(!isFinite(s1)){ + return s2; // Number + }else if(!isFinite(s2)){ + return s1; // Number + } + return (s1 + s2) / 2; // Number + }; + + var transpose = function(/* dojox.gfx.matrix.Matrix2D */ matrix){ + // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object + var M = new m.Matrix2D(matrix); + return Object.extend(M, {dx: 0, dy: 0, xy: M.yx, yx: M.xy}); // dojox.gfx.matrix.Matrix2D + }; + + var scaleSign = function(/* dojox.gfx.matrix.Matrix2D */ matrix){ + return (matrix.xx * matrix.yy < 0 || matrix.xy * matrix.yx > 0) ? -1 : 1; // Number + }; + + var eigenvalueDecomposition = function(/* dojox.gfx.matrix.Matrix2D */ matrix){ + // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object + var M = m.normalize(matrix), + b = -M.xx - M.yy, + c = M.xx * M.yy - M.xy * M.yx, + d = Math.sqrt(b * b - 4 * c), + l1 = -(b + (b < 0 ? -d : d)) / 2, + l2 = c / l1, + vx1 = M.xy / (l1 - M.xx), vy1 = 1, + vx2 = M.xy / (l2 - M.xx), vy2 = 1; + if(eq(l1, l2)){ + vx1 = 1, vy1 = 0, vx2 = 0, vy2 = 1; + } + if(!isFinite(vx1)){ + vx1 = 1, vy1 = (l1 - M.xx) / M.xy; + if(!isFinite(vy1)){ + vx1 = (l1 - M.yy) / M.yx, vy1 = 1; + if(!isFinite(vx1)){ + vx1 = 1, vy1 = M.yx / (l1 - M.yy); + } + } + } + if(!isFinite(vx2)){ + vx2 = 1, vy2 = (l2 - M.xx) / M.xy; + if(!isFinite(vy2)){ + vx2 = (l2 - M.yy) / M.yx, vy2 = 1; + if(!isFinite(vx2)){ + vx2 = 1, vy2 = M.yx / (l2 - M.yy); + } + } + } + var d1 = Math.sqrt(vx1 * vx1 + vy1 * vy1), + d2 = Math.sqrt(vx2 * vx2 + vy2 * vy2); + if(isNaN(vx1 /= d1)){ vx1 = 0; } + if(isNaN(vy1 /= d1)){ vy1 = 0; } + if(isNaN(vx2 /= d2)){ vx2 = 0; } + if(isNaN(vy2 /= d2)){ vy2 = 0; } + return { // Object + value1: l1, + value2: l2, + vector1: {x: vx1, y: vy1}, + vector2: {x: vx2, y: vy2} + }; + }; + + var decomposeSR = function(/* dojox.gfx.matrix.Matrix2D */ M, /* Object */ result){ + // summary: decomposes a matrix into [scale, rotate]; no checks are done. + var sign = scaleSign(M), + a = result.angle1 = (Math.atan2(M.yx, M.yy) + Math.atan2(-sign * M.xy, sign * M.xx)) / 2, + cos = Math.cos(a), sin = Math.sin(a); + result.sx = calcFromValues(M.xx / cos, -M.xy / sin); + result.sy = calcFromValues(M.yy / cos, M.yx / sin); + return result; // Object + }; + + var decomposeRS = function(/* dojox.gfx.matrix.Matrix2D */ M, /* Object */ result){ + // summary: decomposes a matrix into [rotate, scale]; no checks are done + var sign = scaleSign(M), + a = result.angle2 = (Math.atan2(sign * M.yx, sign * M.xx) + Math.atan2(-M.xy, M.yy)) / 2, + cos = Math.cos(a), sin = Math.sin(a); + result.sx = calcFromValues(M.xx / cos, M.yx / sin); + result.sy = calcFromValues(M.yy / cos, -M.xy / sin); + return result; // Object + }; + + return function(matrix){ + // summary: decompose a 2D matrix into translation, scaling, and rotation components + // description: this function decompose a matrix into four logical components: + // translation, rotation, scaling, and one more rotation using SVD. + // The components should be applied in following order: + // | [translate, rotate(angle2), scale, rotate(angle1)] + // matrix: dojox.gfx.matrix.Matrix2D: a 2D matrix-like object + var M = m.normalize(matrix), + result = {dx: M.dx, dy: M.dy, sx: 1, sy: 1, angle1: 0, angle2: 0}; + // detect case: [scale] + if(eq(M.xy, 0) && eq(M.yx, 0)){ + return Object.extend(result, {sx: M.xx, sy: M.yy}); // Object + } + // detect case: [scale, rotate] + if(eq(M.xx * M.yx, -M.xy * M.yy)){ + return decomposeSR(M, result); // Object + } + // detect case: [rotate, scale] + if(eq(M.xx * M.xy, -M.yx * M.yy)){ + return decomposeRS(M, result); // Object + } + // do SVD + var MT = transpose(M), + u = eigenvalueDecomposition([M, MT]), + v = eigenvalueDecomposition([MT, M]), + U = new m.Matrix2D({xx: u.vector1.x, xy: u.vector2.x, yx: u.vector1.y, yy: u.vector2.y}), + VT = new m.Matrix2D({xx: v.vector1.x, xy: v.vector1.y, yx: v.vector2.x, yy: v.vector2.y}), + S = new m.Matrix2D([m.invert(U), M, m.invert(VT)]); + decomposeSR(VT, result); + S.xx *= result.sx; + S.yy *= result.sy; + decomposeRS(U, result); + S.xx *= result.sx; + S.yy *= result.sy; + return Object.extend(result, {sx: S.xx, sy: S.yy}); // Object + }; + })(), + + + /** + Sets the canvas context ctx's transformation matrix to m, with ctm being + the current transformation matrix. + */ + setTransform : function(ctx, m, ctm) { + if (ctx.setTransform) + return ctx.setTransform.apply(ctx, m) + this.transform(ctx, this.tInvertMatrix(ctm)) + this.transform(ctx, m) + }, + + /** + Skews the canvas context by angle on the x-axis. + */ + skewX : function(ctx, angle) { + return this.transform(ctx, this.tSkewXMatrix(angle)) + }, + + /** + Skews the canvas context by angle on the y-axis. + */ + skewY : function(ctx, angle) { + return this.transform(ctx, this.tSkewYMatrix(angle)) + }, + + /** + Rotates a transformation matrix by angle. + */ + tRotate : function(m1, angle) { + // return this.tMatrixMultiply(matrix, this.tRotationMatrix(angle)) + var c = Math.cos(angle) + var s = Math.sin(angle) + var m11 = m1[0]*c + m1[2]*s + var m12 = m1[1]*c + m1[3]*s + var m21 = m1[0]*-s + m1[2]*c + var m22 = m1[1]*-s + m1[3]*c + m1[0] = m11 + m1[1] = m12 + m1[2] = m21 + m1[3] = m22 + return m1 + }, + + /** + Translates a transformation matrix by x and y. + */ + tTranslate : function(m1, x, y) { + // return this.tMatrixMultiply(matrix, this.tTranslationMatrix(x,y)) + m1[4] += m1[0]*x + m1[2]*y + m1[5] += m1[1]*x + m1[3]*y + return m1 + }, + + /** + Scales a transformation matrix by sx and sy. + */ + tScale : function(m1, sx, sy) { + // return this.tMatrixMultiply(matrix, this.tScalingMatrix(sx,sy)) + m1[0] *= sx + m1[1] *= sx + m1[2] *= sy + m1[3] *= sy + return m1 + }, + + /** + Skews a transformation matrix by angle on the x-axis. + */ + tSkewX : function(m1, angle) { + return this.tMatrixMultiply(m1, this.tSkewXMatrix(angle)) + }, + + /** + Skews a transformation matrix by angle on the y-axis. + */ + tSkewY : function(m1, angle) { + return this.tMatrixMultiply(m1, this.tSkewYMatrix(angle)) + }, + + /** + Returns a 3x2 2D column-major y-skew matrix for the angle. + */ + tSkewXMatrix : function(angle) { + return [ 1, 0, Math.tan(angle), 1, 0, 0 ] + }, + + /** + Returns a 3x2 2D column-major y-skew matrix for the angle. + */ + tSkewYMatrix : function(angle) { + return [ 1, Math.tan(angle), 0, 1, 0, 0 ] + }, + + /** + Returns a 3x2 2D column-major rotation matrix for the angle. + */ + tRotationMatrix : function(angle) { + var c = Math.cos(angle) + var s = Math.sin(angle) + return [ c, s, -s, c, 0, 0 ] + }, + + /** + Returns a 3x2 2D column-major translation matrix for x and y. + */ + tTranslationMatrix : function(x, y) { + return [ 1, 0, 0, 1, x, y ] + }, + + /** + Returns a 3x2 2D column-major scaling matrix for sx and sy. + */ + tScalingMatrix : function(sx, sy) { + return [ sx, 0, 0, sy, 0, 0 ] + }, + + /** + Returns the name of the text backend to use. + + Possible values are: + * 'MozText' for Firefox + * 'DrawString' for Rhino + * 'NONE' no text drawing + + @return The text backend name + @type String + */ + getTextBackend : function() { + if (this.textBackend == null) + this.textBackend = this.detectTextBackend() + return this.textBackend + }, + + /** + Detects the name of the text backend to use. + + Possible values are: + * 'MozText' for Firefox + * 'DrawString' for Rhino + * 'NONE' no text drawing + + @return The text backend name + @type String + */ + detectTextBackend : function() { + var ctx = this.getTestContext() + if (ctx.fillText) { + return 'HTML5' + } else if (ctx.mozDrawText) { + return 'MozText' + } else if (ctx.drawString) { + return 'DrawString' + } + return 'NONE' + }, + + getSupportsPutImageData : function() { + if (this.supportsPutImageData == null) { + var ctx = this.getTestContext() + var support = ctx.putImageData + if (support) { + try { + var idata = ctx.getImageData(0,0,1,1) + idata[0] = 255 + idata[1] = 0 + idata[2] = 255 + idata[3] = 255 + ctx.putImageData({width: 1, height: 1, data: idata}, 0, 0) + var idata = ctx.getImageData(0,0,1,1) + support = [255, 0, 255, 255].equals(idata.data) + } catch(e) { + support = false + } + } + this.supportsPutImageData = support + } + return support + }, + + /** + Returns true if the browser can be coaxed to work with + {@link CanvasSupport.isPointInPath}. + + @return Whether the browser supports isPointInPath or not + @type boolean + */ + getSupportsIsPointInPath : function() { + if (this.supportsIsPointInPath == null) + this.supportsIsPointInPath = !!this.getTestContext().isPointInPath + return this.supportsIsPointInPath + }, + + /** + Returns the coordinate system in which the isPointInPath of the + browser operates. Possible coordinate systems are + CanvasSupport.DEVICE_SPACE and CanvasSupport.USER_SPACE. + + @return The coordinate system for the browser's isPointInPath + */ + getIsPointInPathMode : function() { + if (this.isPointInPathMode == null) + this.isPointInPathMode = this.detectIsPointInPathMode() + return this.isPointInPathMode + }, + + /** + Detects the coordinate system in which the isPointInPath of the + browser operates. Possible coordinate systems are + CanvasSupport.DEVICE_SPACE and CanvasSupport.USER_SPACE. + + @return The coordinate system for the browser's isPointInPath + @private + */ + detectIsPointInPathMode : function() { + var ctx = this.getTestContext() + var rv + if (!ctx.isPointInPath) + return this.USER_SPACE + ctx.save() + ctx.translate(1,0) + ctx.beginPath() + ctx.rect(0,0,1,1) + if (ctx.isPointInPath(0.3,0.3)) { + rv = this.USER_SPACE + } else { + rv = this.DEVICE_SPACE + } + ctx.restore() + return rv + }, + + /** + Returns true if the device-space point (x,y) is inside the fill of + ctx's current path. + + @param ctx Canvas 2D context to query + @param x The distance in pixels from the left side of the canvas element + @param y The distance in pixels from the top side of the canvas element + @param matrix The current transformation matrix. Needed if the browser has + no isPointInPath or the browser's isPointInPath works in + user-space coordinates and the browser doesn't support + setTransform. + @param callbackObj If the browser doesn't support isPointInPath, + callbackObj.isPointInPath will be called with the + x,y-coordinates transformed to user-space. + @param + @return Whether (x,y) is inside ctx's current path or not + @type boolean + */ + isPointInPath : function(ctx, x, y, matrix, callbackObj) { + var rv + if (!ctx.isPointInPath) { + if (callbackObj && callbackObj.isPointInPath) { + var xy = this.tMatrixMultiplyPoint(this.tInvertMatrix(matrix), x, y) + return callbackObj.isPointInPath(xy[0], xy[1]) + } else { + return false + } + } else { + if (this.getIsPointInPathMode() == this.USER_SPACE) { + if (!ctx.setTransform) { + var xy = this.tMatrixMultiplyPoint(this.tInvertMatrix(matrix), x, y) + rv = ctx.isPointInPath(xy[0], xy[1]) + } else { + ctx.save() + ctx.setTransform(1,0,0,1,0,0) + rv = ctx.isPointInPath(x,y) + ctx.restore() + } + } else { + rv = ctx.isPointInPath(x,y) + } + return rv + } + } +} + + +RecordingContext = Klass({ + objectId : 0, + commands : [], + isMockObject : true, + + initialize : function(commands) { + this.commands = commands || [] + Object.conditionalExtend(this, this.getMockContext()) + }, + + getMockContext : function() { + if (!RecordingContext.MockContext) { + var c = E.canvas(1,1) + var ctx = CanvasSupport.getContext(c, '2d') + var obj = {} + for (var i in ctx) { + if (typeof(ctx[i]) == 'function') + obj[i] = this.createRecordingFunction(i) + else + obj[i] = ctx[i] + } + obj.isPointInPath = null + obj.transform = null + obj.setTransform = null + RecordingContext.MockContext = obj + } + return RecordingContext.MockContext + }, + + createRecordingFunction : function(name){ + if (name.search(/^set[A-Z]/) != -1 && name != 'setTransform') { + var varName = name.charAt(3).toLowerCase() + name.slice(4) + return function(){ + this[varName] = arguments[0] + this.commands.push([name, $A(arguments)]) + } + } else { + return function(){ + this.commands.push([name, $A(arguments)]) + } + } + }, + + clear : function(){ + this.commands = [] + }, + + getRecording : function() { + return this.commands + }, + + serialize : function(width, height) { + return '(' + { + width: width, height: height, + commands: this.getRecording() + }.toSource() + ')' + }, + + play : function(ctx) { + RecordingContext.play(ctx, this.getRecording()) + }, + + createLinearGradient : function() { + var id = this.objectId++ + this.commands.push([id, '=', 'createLinearGradient', $A(arguments)]) + return new MockGradient(this, id) + }, + + createRadialGradient : function() { + var id = this.objectId++ + this.commands.push([id, '=', 'createRadialGradient', $A(arguments)]) + return new this.MockGradient(this, id) + }, + + createPattern : function() { + var id = this.objectId++ + this.commands.push([id, '=', 'createPattern', $A(arguments)]) + return new this.MockGradient(this, id) + }, + + MockGradient : Klass({ + isMockObject : true, + + initialize : function(recorder, id) { + this.recorder = recorder + this.id = id + }, + + addColorStop : function() { + this.recorder.commands.push([this.id, 'addColorStop', $A(arguments)]) + }, + + toSource : function() { + return {id : this.id, isMockObject : true}.toSource() + } + }) +}) +RecordingContext.play = function(ctx, commands) { + var dictionary = [] + for (var i=0; i k[i-1].time and object.time < k[i].time: + object.state = k[i].tween(position, k[i-1].state, k[i].state) + where position = elapsed / duration, + elapsed = object.time - k[i-1].time, + duration = k[i].time - k[i-1].time + */ +Timeline = Klass({ + startTime : null, + repeat : false, + lastAction : 0, + + initialize : function(repeat, pingpong) { + this.repeat = repeat + this.keyframes = [] + }, + + addKeyframe : function(time, target, tween) { + if (arguments.length == 1) this.keyframes.push(time) + else this.keyframes.push({ + time : time, + target : target, + tween : tween + }) + }, + + appendKeyframe : function(timeDelta, target, tween) { + this.lastAction += timeDelta + return this.addKeyframe(this.lastAction, target, tween) + }, + + evaluate : function(object, ot, dt) { + if (this.startTime == null) this.startTime = ot + var t = ot - this.startTime + if (this.keyframes.length > 0) { + // find current keyframe + var currentIndex, previousFrame, currentFrame + for (var i=0; i t) { + currentIndex = i + break + } + } + if (currentIndex != null) { + previousFrame = this.keyframes[currentIndex-1] + currentFrame = this.keyframes[currentIndex] + } + if (!currentFrame) { + if (!this.keyframes.atEnd) { + this.keyframes.atEnd = true + previousFrame = this.keyframes[this.keyframes.length - 1] + Object.extend(object, Object.clone(previousFrame.target)) + if (this.repeat) this.startTime = ot + object.changed = true + } + } else if (previousFrame) { + this.keyframes.atEnd = false + // animate towards current keyframe + var elapsed = t - previousFrame.time + var duration = currentFrame.time - previousFrame.time + var pos = elapsed / duration + for (var k in currentFrame.target) { + if (previousFrame.target[k] != null) { + object.tweenVariable(k, + previousFrame.target[k], currentFrame.target[k], + pos, currentFrame.tween) + } + } + } + } + } + +}) + + +Animatable = Klass({ + tweenFunctions : { + linear : function(v) { return v }, + + set : function(v) { return Math.floor(v) }, + discrete : function(v) { return Math.floor(v) }, + + sine : function(v) { return 0.5-0.5*Math.cos(v*Math.PI) }, + + sproing : function(v) { + return (0.5-0.5*Math.cos(v*3.59261946538606)) * 1.05263157894737 + // pi + pi-acos(0.9) + }, + + square : function(v) { + return v*v + }, + + cube : function(v) { + return v*v*v + }, + + sqrt : function(v) { + return Math.sqrt(v) + }, + + curt : function(v) { + return Math.pow(v, -0.333333333333) + } + }, + + initialize : function() { + this.lastAction = 0 + this.timeline = [] + this.keyframes = [] + this.pendingKeyframes = [] + this.pendingTimelineEvents = [] + this.timelines = [] + this.animators = [] + this.addFrameListener(this.updateTimelines) + this.addFrameListener(this.updateKeyframes) + this.addFrameListener(this.updateTimeline) + this.addFrameListener(this.updateAnimators) + }, + + updateTimelines : function(t, dt) { + for (var i=0; i 0) { + // find current keyframe + var currentIndex, previousFrame, currentFrame + for (var i=0; i t) { + currentIndex = i + break + } + } + if (currentIndex != null) { + previousFrame = this.keyframes[currentIndex-1] + currentFrame = this.keyframes[currentIndex] + } + if (!currentFrame) { + if (!this.keyframes.atEnd) { + this.keyframes.atEnd = true + previousFrame = this.keyframes[this.keyframes.length - 1] + Object.extend(this, Object.clone(previousFrame.target)) + this.changed = true + } + } else if (previousFrame) { + this.keyframes.atEnd = false + // animate towards current keyframe + var elapsed = t - previousFrame.time + var duration = currentFrame.time - previousFrame.time + var pos = elapsed / duration + for (var k in currentFrame.target) { + if (previousFrame.target[k] != null) { + this.tweenVariable(k, + previousFrame.target[k], currentFrame.target[k], + pos, currentFrame.tween) + } + } + } + } + }, + + addPendingKeyframes : function(t) { + if (this.pendingKeyframes.length > 0) { + while (this.pendingKeyframes.length > 0) { + var kf = this.pendingKeyframes.shift() + if (kf.time == null) + kf.time = kf.relativeTime + t + this.keyframes.push(kf) + } + this.keyframes.stableSort(function(a,b) { return a.time - b.time }) + } + }, + + /** + Run and remove timelineEvents that have startTime <= t. + TimelineEvents are run in the ascending order of their startTimes. + */ + updateTimeline : function(t, dt) { + this.addPendingTimelineEvents(t) + while (this.timeline[0] && this.timeline[0].startTime <= t) { + var keyframe = this.timeline.shift() + var rv = true + if (typeof(keyframe.action) == 'function') + rv = keyframe.action.call(this, t, dt, keyframe) + else + this.animators.push(keyframe.action) + if (keyframe.repeatEvery != null && rv != false) { + if (keyframe.repeatTimes != null) { + if (keyframe.repeatTimes <= 0) continue + keyframe.repeatTimes-- + } + keyframe.startTime += keyframe.repeatEvery + this.addTimelineEvent(keyframe) + } + this.changed = true + } + }, + + addPendingTimelineEvents : function(t) { + if (this.pendingTimelineEvents.length > 0) { + while (this.pendingTimelineEvents.length > 0) { + var kf = this.pendingTimelineEvents.shift() + if (!kf.startTime) + kf.startTime = kf.relativeStartTime + t + this.timeline.push(kf) + } + this.timeline.stableSort(function(a,b) { return a.startTime - b.startTime }) + } + }, + + addTimelineEvent : function(kf) { + this.pendingTimelineEvents.push(kf) + }, + + /** + Run each animator, delete ones that have their durations exceeded. + */ + updateAnimators : function(t, dt) { + for (var i=0; i= 1) { + if (!ani.repeat) { + pos = 1 + shouldRemove = true + } else { + if (ani.repeat !== true) ani.repeat = Math.max(0, ani.repeat - 1) + if (ani.accumulate) { + ani.startValue = Object.clone(ani.endValue) + ani.endValue = Object.sum(ani.difference, ani.endValue) + } + if (ani.repeat == 0) { + shouldRemove = true + pos = 1 + } else { + ani.startTime = t + pos = pos % 1 + } + } + } else if (ani.repeat && ani.repeat !== true && ani.repeat <= pos) { + shouldRemove = true + pos = ani.repeat + } + this.tweenVariable(ani.variable, ani.startValue, ani.endValue, pos, ani.tween) + if (shouldRemove) { + this.animators.splice(i, 1) + i-- + } + } + }, + + tweenVariable : function(variable, start, end, pos, tweenFunction) { + if (typeof(tweenFunction) != 'function') { + tweenFunction = this.tweenFunctions[tweenFunction] || this.tweenFunctions.linear + } + var tweened = tweenFunction(pos) + if (typeof(variable) != 'function') { + if (start instanceof Array) { + for (var j=0; j= duration) { + callback.call(this) + this.removeFrameListener(animator) + } + elapsed++ + } + this.addFrameListener(animator) + return animator + }, + + everyFrame : function(duration, callback, noFirst) { + var elapsed = noFirst ? 0 : duration + var animator + animator = function(t, dt){ + if (elapsed >= duration) { + if (callback.call(this) == false) + this.removeFrameListener(animator) + elapsed = 0 + } + elapsed++ + } + this.addFrameListener(animator) + return animator + } +}) +Animatable.uid = 0 + + + +/** + CanvasNode is the base CAKE scenegraph node. All the other scenegraph nodes + derive from it. A plain CanvasNode does no drawing, but it can be used for + grouping other nodes and setting up the group's drawing state. + + var scene = new CanvasNode({x: 10, y: 10}) + + The usual way to use CanvasNodes is to append them to a Canvas object: + + var scene = new CanvasNode() + scene.append(new Rectangle(40, 40, {fill: true})) + var elem = E.canvas(400, 400) + var canvas = new Canvas(elem) + canvas.append(scene) + + You can also use CanvasNodes to draw directly to a canvas element: + + var scene = new CanvasNode() + scene.append(new Circle(40, {x:200, y:200, stroke: true})) + var elem = E.canvas(400, 400) + scene.handleDraw(elem.getContext('2d')) + + */ +CanvasNode = Klass(Animatable, Transformable, { + OBJECTBOUNDINGBOX : 'objectBoundingBox', + + // whether to draw the node and its childNodes or not + visible : true, + + // whether to draw the node (doesn't affect subtree) + drawable : true, + + // the CSS display property can be used to affect 'visible' + // false => visible = visible + // 'none' => visible = false + // otherwise => visible = true + display : null, + + // the CSS visibility property can be used to affect 'drawable' + // false => drawable = drawable + // 'hidden' => drawable = false + // otherwise => drawable = true + visibility : null, + + // whether this and the subtree from this register mouse hover + catchMouse : true, + + // Whether this object registers mouse hover. Only set this to true when you + // have a drawable object that can be picked. Otherwise the object requires + // a matrix inversion on Firefox 2 and Safari, which is slow. + pickable : false, + + // true if this node or one of its descendants is under the mouse + // cursor and catchMouse is true + underCursor : false, + + // zIndex in relation to sibling nodes (note: not global) + zIndex : 0, + + // x translation of the node + x : 0, + + // y translation of the node + y : 0, + + // scale factor: number for uniform scaling, [x,y] for dimension-wise + scale : 1, + + // Rotation of the node, in radians. + // + // The rotation can also be the array [angle, cx, cy], + // where cx and cy define the rotation center. + // + // The array form is equivalent to + // translate(cx, cy); rotate(angle); translate(-cx, -cy); + rotation : 0, + + // Transform matrix with which to multiply the current transform matrix. + // Applied after all other transformations. + matrix : null, + + // Transform matrix with which to replace the current transform matrix. + // Applied before any other transformation. + absoluteMatrix : null, + + // SVG-like list of transformations to apply. + // The different transformations are: + // ['translate', [x,y]] + // ['rotate', [angle, cx, cy]] - (optional) cx and cy are the rotation center + // ['scale', [x,y]] + // ['matrix', [m11, m12, m21, m22, dx, dy]] + transformList : null, + + // fillStyle for the node and its descendants + // Possibilities: + // null // use the previous + // true // use the previous but do fill + // false // use the previous but don't do fill + // 'none' // use the previous but don't do fill + // + // 'white' + // '#fff' + // '#ffffff' + // 'rgba(255,255,255, 1.0)' + // [255, 255, 255, 1.0] + // new Gradient(...) + // new Pattern(myImage, 'no-repeat') + fill : null, + + // strokeStyle for the node and its descendants + // Possibilities: + // null // use the previous + // true // use the previous but do stroke + // false // use the previous but don't do stroke + // 'none' // use the previous but don't do stroke + // + // 'white' + // '#fff' + // '#ffffff' + // 'rgba(255,255,255, 1.0)' + // [255, 255, 255, 1.0] + // new Gradient(...) + // new Pattern(myImage, 'no-repeat') + stroke : null, + + // stroke line width + strokeWidth : null, + + // stroke line cap style ('butt' | 'round' | 'square') + lineCap : null, + + // stroke line join style ('bevel' | 'round' | 'miter') + lineJoin : null, + + // stroke line miter limit + miterLimit : null, + + // set globalAlpha to this value + absoluteOpacity : null, + + // multiply globalAlpha by this value + opacity : null, + + // fill opacity + fillOpacity : null, + + // stroke opacity + strokeOpacity : null, + + // set globalCompositeOperation to this value + // Possibilities: + // ( 'source-over' | + // 'copy' | + // 'lighter' | + // 'darker' | + // 'xor' | + // 'source-in' | + // 'source-out' | + // 'destination-over' | + // 'destination-atop' | + // 'destination-in' | + // 'destination-out' ) + compositeOperation : null, + + // Color for the drop shadow + shadowColor : null, + + // Drop shadow blur radius + shadowBlur : null, + + // Drop shadow's x-offset + shadowOffsetX : null, + + // Drop shadow's y-offset + shadowOffsetY : null, + + // HTML5 text API + font : null, + // horizontal position of the text origin + // 'left' | 'center' | 'right' | 'start' | 'end' + textAlign : null, + // vertical position of the text origin + // 'top' | 'hanging' | 'middle' | 'alphabetic' | 'ideographic' | 'bottom' + textBaseline : null, + + cursor : null, + + changed : true, + + tagName : 'g', + + getNextSibling : function(){ + if (this.parentNode) + return this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this)+1] + return null + }, + + getPreviousSibling : function(){ + if (this.parentNode) + return this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this)-1] + return null + }, + + /** + Initialize the CanvasNode and merge an optional config hash. + */ + initialize : function(config) { + this.root = this + this.currentMatrix = [1,0,0,1,0,0] + this.previousMatrix = [1,0,0,1,0,0] + this.needMatrixUpdate = true + this.childNodes = [] + this.frameListeners = [] + this.eventListeners = {} + Animatable.initialize.call(this) + if (config) + Object.extend(this, config) + }, + + /** + Create a clone of the node and its subtree. + */ + clone : function() { + var c = Object.clone(this) + c.parent = c.root = null + for (var i in this) { + if (typeof(this[i]) == 'object') + c[i] = Object.clone(this[i]) + } + c.parent = c.root = null + c.childNodes = [] + c.setRoot(null) + for (var i=0; i=0; i--) + if (!path[i].handleEvent(event)) return false + event.canvasPhase = 'bubble' + for (var i=0; i 0) { + var c0 = c.pop() + if (c0.underCursor) { + c0.underCursor = false + Array.prototype.push.apply(c, c0.childNodes) + } + } + } + }, + + __zSort : function(c) { + c.stableSort(function(c1,c2) { return c1.zIndex - c2.zIndex; }); + }, + + __getChildrenCopy : function() { + if (this.__childNodesCopy) { + while (this.__childNodesCopy.length > this.childNodes.length) + this.__childNodesCopy.pop() + for (var i=0; i bb2[0]) bb[0] = bb2[0] + if (bb[1] > bb2[1]) bb[1] = bb2[1] + if (bb[2]+bb[0] < bb2[2]+bb2[0]) bb[2] = bb2[2]+bb2[0]-bb[0] + if (bb[3]+bb[1] < bb2[3]+bb2[1]) bb[3] = bb2[3]+bb2[1]-bb[1] + }, + + getAxisAlignedBoundingBox : function() { + this.transform(null, true) + if (!this.getBoundingBox) return null + var bbox = this.getBoundingBox() + var xy1 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix, + bbox[0], bbox[1]) + var xy2 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix, + bbox[0]+bbox[2], bbox[1]+bbox[3]) + var xy3 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix, + bbox[0], bbox[1]+bbox[3]) + var xy4 = CanvasSupport.tMatrixMultiplyPoint(this.currentMatrix, + bbox[0]+bbox[2], bbox[1]) + var x1 = Math.min(xy1[0], xy2[0], xy3[0], xy4[0]) + var x2 = Math.max(xy1[0], xy2[0], xy3[0], xy4[0]) + var y1 = Math.min(xy1[1], xy2[1], xy3[1], xy4[1]) + var y2 = Math.max(xy1[1], xy2[1], xy3[1], xy4[1]) + return [x1, y1, x2-x1, y2-y1] + }, + + makeDraggable : function() { + this.addEventListener('dragstart', function(ev) { + this.dragStartPosition = {x: this.x, y: this.y}; + ev.stopPropagation(); + ev.preventDefault(); + return false; + }, false); + this.addEventListener('drag', function(ev) { + this.x = this.dragStartPosition.x + this.root.dragX / this.parent.currentMatrix[0]; + this.y = this.dragStartPosition.y + this.root.dragY / this.parent.currentMatrix[3]; + ev.stopPropagation(); + ev.preventDefault(); + return false; + }, false); + } +}) + + +/** + Canvas is the canvas manager class. + It takes care of updating and drawing its childNodes on a canvas element. + + An example with a rotating rectangle: + + var c = E.canvas(500, 500) + var canvas = new Canvas(c) + var rect = new Rectangle(100, 100) + rect.x = 250 + rect.y = 250 + rect.fill = true + rect.fillStyle = 'green' + rect.addFrameListener(function(t) { + this.rotation = ((t / 3000) % 1) * Math.PI * 2 + }) + canvas.append(rect) + document.body.appendChild(c) + + + To use the canvas as a manually updated image: + + var canvas = new Canvas(E.canvas(200,40), { + isPlaying : false, + redrawOnlyWhenChanged : true + }) + var c = new Circle(20) + c.x = 100 + c.y = 20 + c.fill = true + c.fillStyle = 'red' + c.addFrameListener(function(t) { + if (this.root.absoluteMouseX != null) { + this.x = this.root.mouseX // relative to canvas surface + this.root.changed = true + } + }) + canvas.append(c) + + + Or by using raw onFrame-calls: + + var canvas = new Canvas(E.canvas(200,40), { + isPlaying : false, + fill : true, + fillStyle : 'white' + }) + var c = new Circle(20) + c.x = 100 + c.y = 20 + c.fill = true + c.fillStyle = 'red' + canvas.append(c) + canvas.onFrame() + + + Which is also the recommended way to use a canvas inside another canvas: + + var canvas = new Canvas(E.canvas(200,40), { + isPlaying : false + }) + var c = new Circle(20, { + x: 100, y: 20, + fill: true, fillStyle: 'red' + }) + canvas.append(c) + + var topCanvas = new Canvas(E.canvas(500, 500)) + var canvasImage = new ImageNode(canvas.canvas, {x: 250, y: 250}) + topCanvas.append(canvasImage) + canvasImage.addFrameListener(function(t) { + this.rotation = (t / 3000 % 1) * Math.PI * 2 + canvas.onFrame(t) + }) + + */ +Canvas = Klass(CanvasNode, { + + clear : true, + frameLoop : false, + recording : false, + opacity : 1, + frame : 0, + elapsed : 0, + frameDuration : 30, + speed : 1.0, + time : 0, + fps : 0, + currentRealFps : 0, + currentFps : 0, + fpsFrames : 30, + startTime : 0, + realFps : 0, + fixedTimestep : false, + playOnlyWhenFocused : true, + isPlaying : true, + redrawOnlyWhenChanged : false, + changed : true, + drawBoundingBoxes : false, + cursor : 'default', + + mouseDown : false, + mouseEvents : [], + + // absolute pixel coordinates from canvas top-left + absoluteMouseX : null, + absoluteMouseY : null, + + /* + Coordinates relative to the canvas's surface scale. + Example: + canvas.width + #=> 100 + canvas.style.width + #=> '100px' + canvas.absoluteMouseX + #=> 50 + canvas.mouseX + #=> 50 + + canvas.style.width = '200px' + canvas.width + #=> 100 + canvas.absoluteMouseX + #=> 100 + canvas.mouseX + #=> 50 + */ + mouseX : null, + mouseY : null, + + elementNodeZIndexCounter : 0, + + initialize : function(canvas, config) { + if (arguments.length > 2) { + var container = arguments[0] + var w = arguments[1] + var h = arguments[2] + var config = arguments[3] + var canvas = E.canvas(w,h) + var canvasContainer = E('div', canvas, {style: + {overflow:'hidden', width:w+'px', height:h+'px', position:'relative'} + }) + this.canvasContainer = canvasContainer + if (container) + container.appendChild(canvasContainer) + } + CanvasNode.initialize.call(this, config) + this.mouseEventStack = [] + this.canvas = canvas + canvas.canvas = this + this.width = this.canvas.width + this.height = this.canvas.height + var th = this + this.frameHandler = function() { th.onFrame() } + this.canvas.addEventListener('DOMNodeInserted', function(ev) { + if (ev.target == this) + th.addEventListeners() + }, false) + this.canvas.addEventListener('DOMNodeRemoved', function(ev) { + if (ev.target == this) + th.removeEventListeners() + }, false) + if (this.canvas.parentNode) this.addEventListeners() + this.startTime = new Date().getTime() + if (this.isPlaying) + this.play() + }, + + // FIXME + removeEventListeners : function() { + }, + + addEventListeners : function() { + var th = this + this.canvas.parentNode.addMouseEvent = function(e){ + var xy = Mouse.getRelativeCoords(this, e) + th.absoluteMouseX = xy.x + th.absoluteMouseY = xy.y + var style = document.defaultView.getComputedStyle(th.canvas,"") + var w = parseFloat(style.getPropertyValue('width')) + var h = parseFloat(style.getPropertyValue('height')) + th.mouseX = th.absoluteMouseX * (w / th.canvas.width) + th.mouseY = th.absoluteMouseY * (h / th.canvas.height) + th.addMouseEvent(th.mouseX, th.mouseY, th.mouseDown) + } + this.canvas.parentNode.contains = this.contains + + this.canvas.parentNode.addEventListener('mousedown', function(e) { + th.mouseDown = true + if (th.keyTarget != th.target) { + if (th.keyTarget) + th.dispatchEvent({type: 'blur', canvasTarget: th.keyTarget}) + th.keyTarget = th.target + if (th.keyTarget) + th.dispatchEvent({type: 'focus', canvasTarget: th.keyTarget}) + } + this.addMouseEvent(e) + }, true) + + this.canvas.parentNode.addEventListener('mouseup', function(e) { + this.addMouseEvent(e) + th.mouseDown = false + }, true) + + this.canvas.parentNode.addEventListener('mousemove', function(e) { + this.addMouseEvent(e) + if (th.prevClientX == null) { + th.prevClientX = e.clientX + th.prevClientY = e.clientY + } + if (th.dragTarget) { + var nev = document.createEvent('MouseEvents') + nev.initMouseEvent('drag', true, true, window, e.detail, + e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, + e.shiftKey, e.metaKey, e.button, e.relatedTarget) + nev.canvasTarget = th.dragTarget + nev.dx = e.clientX - th.prevClientX + nev.dy = e.clientY - th.prevClientY + th.dragX += nev.dx + th.dragY += nev.dy + th.dispatchEvent(nev) + } + if (!th.mouseDown) { + if (th.dragTarget) { + var nev = document.createEvent('MouseEvents') + nev.initMouseEvent('dragend', true, true, window, e.detail, + e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, + e.shiftKey, e.metaKey, e.button, e.relatedTarget) + nev.canvasTarget = th.dragTarget + th.dispatchEvent(nev) + th.dragX = th.dragY = 0 + th.dragTarget = false + } + } else if (!th.dragTarget && th.target) { + th.dragTarget = th.target + var nev = document.createEvent('MouseEvents') + nev.initMouseEvent('dragstart', true, true, window, e.detail, + e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, + e.shiftKey, e.metaKey, e.button, e.relatedTarget) + nev.canvasTarget = th.dragTarget + th.dragStartX = e.clientX + th.dragStartY = e.clientY + th.dragX = th.dragY = 0 + th.dispatchEvent(nev) + } + th.prevClientX = e.clientX + th.prevClientY = e.clientY + }, true) + + this.canvas.parentNode.addEventListener('mouseout', function(e) { + if (!CanvasNode.contains.call(this, e.relatedTarget)) + th.absoluteMouseX = th.absoluteMouseY = th.mouseX = th.mouseY = null + }, true) + + var dispatch = this.dispatchEvent.bind(this) + var types = [ + 'mousemove', 'mouseover', 'mouseout', + 'click', 'dblclick', + 'mousedown', 'mouseup', + 'keypress', 'keydown', 'keyup', + 'DOMMouseScroll', 'mousewheel', 'mousemultiwheel', 'textInput', + 'focus', 'blur' + ] + for (var i=0; i 0) { + return this.mouseEventStack.pop() + } else { + return [null, null, null] + } + }, + + freeMouseEvent : function(ev) { + this.mouseEventStack.push(ev) + if (this.mouseEventStack.length > 100) + this.mouseEventStack.splice(0,this.mouseEventStack.length) + }, + + clearMouseEvents : function() { + while (this.mouseEvents.length > 0) + this.freeMouseEvent(this.mouseEvents.pop()) + }, + + /** + Start frame loop. + + The frame loop is an interval, where #onFrame is called every + #frameDuration milliseconds. + */ + play : function() { + this.stop() + this.realTime = new Date().getTime() + this.frameLoop = setInterval(this.frameHandler, this.frameDuration) + this.isPlaying = true + }, + + /** + Stop frame loop. + */ + stop : function() { + this.__blurStop = false + if (this.frameLoop) { + clearInterval(this.frameLoop) + this.frameLoop = false + } + this.isPlaying = false + }, + + dispatchEvent : function(ev) { + var rv = CanvasNode.prototype.dispatchEvent.call(this, ev) + if (ev.cursor) { + if (this.canvas.style.cursor != ev.cursor) + this.canvas.style.cursor = ev.cursor + } else { + if (this.canvas.style.cursor != this.cursor) + this.canvas.style.cursor = this.cursor + } + return rv + }, + + /** + The frame loop function. Called every #frameDuration milliseconds. + Takes an optional external time parameter (for syncing Canvases with each + other, e.g. when using a Canvas as an image.) + + If the time parameter is given, the second parameter is used as the frame + time delta (i.e. the time elapsed since last frame.) + + If time or timeDelta is not given, the canvas computes its own timeDelta. + + @param time The external time. Optional. + @param timeDelta Time since last frame in milliseconds. Optional. + */ + onFrame : function(time, timeDelta) { + this.elementNodeZIndexCounter = 0 + var ctx = this.getContext() + try { + var realTime = new Date().getTime() + this.currentRealElapsed = (realTime - this.realTime) + this.currentRealFps = 1000 / this.currentRealElapsed + var dt = this.frameDuration * this.speed + if (!this.fixedTimestep) + dt = this.currentRealElapsed * this.speed + this.realTime = realTime + if (time != null) { + this.time = time + if (timeDelta) + dt = timeDelta + } else { + this.time += dt + } + this.previousTarget = this.target + this.target = null + if (this.catchMouse) + this.handlePick(ctx) + if (this.previousTarget != this.target) { + if (this.previousTarget) { + var nev = document.createEvent('MouseEvents') + nev.initMouseEvent('mouseout', true, true, window, + 0, 0, 0, 0, 0, false, false, false, false, 0, null) + nev.canvasTarget = this.previousTarget + this.dispatchEvent(nev) + } + if (this.target) { + var nev = document.createEvent('MouseEvents') + nev.initMouseEvent('mouseover', true, true, window, + 0, 0, 0, 0, 0, false, false, false, false, 0, null) + nev.canvasTarget = this.target + this.dispatchEvent(nev) + } + } + this.handleUpdate(this.time, dt) + this.clearMouseEvents() + if (!this.redrawOnlyWhenChanged || this.changed) { + try { + this.handleDraw(ctx) + } catch(e) { + console.log(e) + throw(e) + } + this.changed = false + } + this.currentElapsed = (new Date().getTime() - this.realTime) + this.elapsed += this.currentElapsed + this.currentFps = 1000 / this.currentElapsed + this.frame++ + if (this.frame % this.fpsFrames == 0) { + this.fps = this.fpsFrames*1000 / (this.elapsed) + this.realFps = this.fpsFrames*1000 / (new Date().getTime() - this.startTime) + this.elapsed = 0 + this.startTime = new Date().getTime() + } + } catch(e) { + if (ctx) { + // screwed up, context is borked + try { + // FIXME don't be stupid + for (var i=0; i<1000; i++) + ctx.restore() + } catch(er) {} + } + delete this.context + throw(e) + } + }, + + /** + Returns the canvas drawing context object. + + @return Canvas drawing context + */ + getContext : function() { + if (this.recording) + return this.getRecordingContext() + else if (this.useMockContext) + return this.getMockContext() + else + return this.get2DContext() + }, + + /** + Gets and returns an augmented canvas 2D drawing context. + + The canvas 2D context is augmented by setter functions for all + its instance variables, making it easier to record canvas operations in + a cross-browser fashion. + */ + get2DContext : function() { + if (!this.context) { + var ctx = CanvasSupport.getContext(this.canvas, '2d') + this.context = ctx + } + return this.context + }, + + /** + Creates and returns a mock drawing context. + + @return Mock drawing context + */ + getMockContext : function() { + if (!this.fakeContext) { + var ctx = this.get2DContext() + this.fakeContext = {} + var f = function(){ return this } + for (var i in ctx) { + if (typeof(ctx[i]) == 'function') + this.fakeContext[i] = f + else + this.fakeContext[i] = ctx[i] + } + this.fakeContext.isMockObject = true + this.fakeContext.addColorStop = f + } + return this.fakeContext + }, + + getRecordingContext : function() { + if (!this.recordingContext) + this.recordingContext = new RecordingContext() + return this.recordingContext + }, + + /** + Canvas drawPickingPath uses the canvas rectangle as its path. + + @param ctx Canvas drawing context + */ + drawPickingPath : function(ctx) { + ctx.rect(0,0, this.canvas.width, this.canvas.height) + }, + + isPointInPath : function(x,y) { + return ((x >= 0) && (x <= this.canvas.width) && (y >= 0) && (y <= this.canvas.height)) + }, + + /** + Sets globalAlpha to this.opacity and clears the canvas if #clear is set to + true. If #fill is also set to true, fills the canvas rectangle instead of + clearing (using #fillStyle as the color.) + + @param ctx Canvas drawing context + */ + draw : function(ctx) { + ctx.setGlobalAlpha( this.opacity ) + if (this.clear) { + if (ctx.fillOn) { + ctx.beginPath() + ctx.rect(0,0, this.canvas.width, this.canvas.height) + ctx.fill() + } else { + ctx.clearRect(0,0, this.canvas.width, this.canvas.height) + } + } + // set default fill and stroke for the canvas contents + ctx.fillStyle = 'black' + ctx.strokeStyle = 'black' + ctx.fillOn = false + ctx.strokeOn = false + } +}) + + +/** + Hacky link class for emulating . + + The correct way would be to have a real under the cursor while hovering + this, or an imagemap polygon built from the clipped subtree path. + + @param href Link href. + @param target Link target, defaults to _self. + @param config Optional config hash. + */ +LinkNode = Klass(CanvasNode, { + href : null, + target : '_self', + cursor : 'pointer', + + initialize : function(href, target, config) { + this.href = href + if (target) + this.target = target + CanvasNode.initialize.call(this, config) + this.setupLinkEventListeners() + }, + + setupLinkEventListeners : function() { + this.addEventListener('click', function(ev) { + if (ev.button == Mouse.RIGHT) return + var target = this.target + if ((ev.ctrlKey || ev.button == Mouse.MIDDLE) && target == '_self') + target = '_blank' + window.open(this.href, target) + }, false) + } +}) + + +/** + AudioNode is a CanvasNode used to play a sound. + + */ +AudioNode = Klass(CanvasNode, { + ready : false, + autoPlay : false, + playing : false, + paused : false, + pan : 0, + volume : 1, + loop : false, + + transformSound : false, + + initialize : function(filename, params) { + CanvasNode.initialize.call(this, params) + this.filename = filename + this.when('load', this._autoPlaySound) + this.loadSound() + }, + + loadSound : function() { + this.sound = CanvasSupport.getSoundObject() + if (!this.sound) return + var self = this + this.sound.onready = function() { + self.ready = true + self.root.dispatchEvent({type: 'ready', canvasTarget: self}) + } + this.sound.onload = function() { + self.loaded = true + self.root.dispatchEvent({type: 'load', canvasTarget: self}) + } + this.sound.onerror = function() { + self.root.dispatchEvent({type: 'error', canvasTarget: self}) + } + this.sound.onfinish = function() { + if (self.loop) self.play() + else self.stop() + } + this.sound.load(this.filename) + }, + + play : function() { + this.playing = true + this.needPlayUpdate = true + }, + + stop : function() { + this.playing = false + this.needPlayUpdate = true + }, + + pause : function() { + if (this.needPauseUpdate) { + this.needPauseUpdate = false + return + } + this.paused = !this.paused + this.needPauseUpdate = true + }, + + setVolume : function(v) { + this.volume = v + this.needStatusUpdate = true + }, + + setPan : function(p) { + this.pan = p + this.needStatusUpdate = true + }, + + handleUpdate : function() { + CanvasNode.handleUpdate.apply(this, arguments) + if (this.willBeDrawn) { + this.transform(null, true) + if (!this.sound) this.loadSound() + if (this.ready) { + if (this.transformSound) { + var x = this.currentMatrix[4] + var y = this.currentMatrix[5] + var a = this.currentMatrix[2] + var b = this.currentMatrix[3] + var c = this.currentMatrix[0] + var d = this.currentMatrix[1] + var hw = this.root.width * 0.5 + var ys = Math.sqrt(a*a + b*b) + var xs = Math.sqrt(c*c + d*d) + this.setVolume(ys) + this.setPan((x - hw) / hw) + } + if (this.needPauseUpdate) { + this.needPauseUpdate = false + this._pauseSound() + } + if (this.needPlayUpdate) { + this.needPlayUpdate = false + if (this.playing) this._playSound() + else this._stopSound() + } + if (this.needStatusUpdate) { + this._setSoundVolume() + this._setSoundPan() + } + } + } + }, + + _autoPlaySound : function() { + if (this.autoPlay) this.play() + }, + + _setSoundVolume : function() { + this.sound.setVolume(this.volume) + }, + + _setSoundPan : function() { + this.sound.setPan(this.pan) + }, + + _playSound : function() { + if (this.sound.play() == false) + return this.playing = false + this.root.dispatchEvent({type: 'play', canvasTarget: this}) + }, + + _stopSound : function() { + this.sound.stop() + this.root.dispatchEvent({type: 'stop', canvasTarget: this}) + }, + + _pauseSound : function() { + this.sound.pause() + this.root.dispatchEvent({type: this.paused ? 'pause' : 'play', canvasTarget: this}) + } +}) + + +/** + ElementNode is a CanvasNode that has an HTML element as its content. + + The content is added to an absolutely positioned HTML element, which is added + to the root node's canvases parentNode. The content element follows the + current transformation matrix. + + The opacity of the element is set to the globalAlpha of the drawing context + unless #noAlpha is true. + + The font-size of the element is set to the current y-scale unless #noScaling + is true. + + Use ElementNode when you need accessible web content in your animations. + + var e = new ElementNode( + E('h1', 'HERZLICH WILLKOMMEN IM BAHNHOF'), + { + x : 40, + y : 30 + } + ) + e.addFrameListener(function(t) { + this.scale = 1 + 0.5*Math.cos(t/1000) + }) + + @param content An HTML element or string of HTML to use as the content. + @param config Optional config has. + */ +ElementNode = Klass(CanvasNode, { + noScaling : false, + noAlpha : false, + inherit : 'inherit', + align: null, // left | center | right + valign: null, // top | center | bottom + xOffset: 0, + yOffset: 0, + + initialize : function(content, config) { + CanvasNode.initialize.call(this, config) + this.content = content + this.element = E('div', content) + this.element.style.MozTransformOrigin = + this.element.style.webkitTransformOrigin = '0 0' + this.element.style.position = 'absolute' + }, + + clone : function() { + var c = CanvasNode.prototype.clone.call(this) + if (this.content && this.content.cloneNode) + c.content = this.content.cloneNode(true) + c.element = E('div', c.content) + c.element.style.position = 'absolute' + c.element.style.MozTransformOrigin = + c.element.style.webkitTransformOrigin = '0 0' + return c + }, + + setRoot : function(root) { + CanvasNode.setRoot.call(this, root) + if (this.element && this.element.parentNode && this.element.parentNode.removeChild) + this.element.parentNode.removeChild(this.element) + }, + + handleUpdate : function(t, dt) { + CanvasNode.handleUpdate.call(this, t, dt) + if (!this.willBeDrawn || !this.visible || this.display == 'none' || this.visibility == 'hidden' || !this.drawable) { + if (this.element.style.display != 'none') + this.element.style.display = 'none' + } else if (this.element.style.display == 'none') { + this.element.style.display = 'block' + } + }, + + addEventListener : function(event, callback, capture) { + var th = this + var ccallback = function() { callback.apply(th, arguments) } + return this.element.addEventListener(event, ccallback, capture||false) + }, + + removeEventListener : function(event, callback, capture) { + var th = this + var ccallback = function() { callback.apply(th, arguments) } + return this.element.removeEventListener(event, ccallback, capture||false) + }, + + draw : function(ctx) { + if (this.cursor && this.element.style.cursor != this.cursor) + this.element.style.cursor = this.cursor + if (this.element.style.zIndex != this.root.elementNodeZIndexCounter) + this.element.style.zIndex = this.root.elementNodeZIndexCounter + this.root.elementNodeZIndexCounter++ + var baseTransform = this.currentMatrix + xo = this.xOffset + yo = this.yOffset + if (this.fillBoundingBox && this.parent && this.parent.getBoundingBox) { + var bb = this.parent.getBoundingBox() + xo += bb[0] + yo += bb[1] + } + var xy = CanvasSupport.tMatrixMultiplyPoint(baseTransform.slice(0,4).concat([0,0]), + xo, yo) + var x = this.currentMatrix[4] + xy[0] + var y = this.currentMatrix[5] + xy[1] + var a = this.currentMatrix[2] + var b = this.currentMatrix[3] + var c = this.currentMatrix[0] + var d = this.currentMatrix[1] + var ys = Math.sqrt(a*a + b*b) + var xs = Math.sqrt(c*c + d*d) + if (ctx.fontFamily != null) + this.element.style.fontFamily = ctx.fontFamily + + var wkt = CanvasSupport.isCSSTransformSupported() + if (wkt && !this.noScaling) { + this.element.style.MozTransform = + this.element.style.webkitTransform = 'matrix('+baseTransform.join(",")+')' + } else { + this.element.style.MozTransform = + this.element.style.webkitTransform = '' + } + if (ctx.fontSize != null) { + if (this.noScaling || wkt) { + this.element.style.fontSize = ctx.fontSize + 'px' + } else { + this.element.style.fontSize = ctx.fontSize * ys + 'px' + } + } else { + if (this.noScaling || wkt) { + this.element.style.fontSize = 'inherit' + } else { + this.element.style.fontSize = 100 * ys + '%' + } + } + if (this.noAlpha) + this.element.style.opacity = 1 + else + this.element.style.opacity = ctx.globalAlpha + if (!this.element.parentNode && this.root.canvas.parentNode) { + this.element.style.visibility = 'hidden' + this.root.canvas.parentNode.appendChild(this.element) + var hidden = true + } + var fs = this.color || this.fill + if (this.parent) { + if (!fs || !fs.length) + fs = this.parent.color + if (!fs || !fs.length) + fs = this.parent.fill + } + if (!fs || !fs.length) + fs = ctx.fillStyle + if (typeof(fs) == 'string') { + if (fs.search(/^rgba\(/) != -1) { + this.element.style.color = 'rgb(' + + fs.match(/\d+/g).slice(0,3).join(",") + + ')' + } else { + this.element.style.color = fs + } + } else if (fs.length) { + this.element.style.color = 'rgb(' + fs.slice(0,3).map(Math.floor).join(",") + ')' + } + var dx = 0, dy = 0 + if (bb) { + this.element.style.width = Math.floor(xs * bb[2]) + 'px' + this.element.style.height = Math.floor(ys * bb[3]) + 'px' + this.eWidth = xs + this.eHeight = ys + } else { + this.element.style.width = '' + this.element.style.height = '' + var align = this.align || this.textAnchor + var origin = [0,0] + if (align == 'center' || align == 'middle') { + dx = -this.element.offsetWidth / 2 + origin[0] = '50%' + } else if (align == 'right') { + dx = -this.element.offsetWidth + origin[0] = '100%' + } + var valign = this.valign + if (valign == 'center' || valign == 'middle') { + dy = -this.element.offsetHeight / 2 + origin[1] = '50%' + } else if (valign == 'bottom') { + dy = -this.element.offsetHeight + origin[1] = '100%' + } + this.element.style.webkitTransformOrigin = + this.element.style.MozTransformOrigin = origin.join(" ") + this.eWidth = this.element.offsetWidth / xs + this.eHeight = this.element.offsetHeight / ys + } + if (wkt && !this.noScaling) { + this.element.style.left = Math.floor(dx) + 'px' + this.element.style.top = Math.floor(dy) + 'px' + } else { + this.element.style.left = Math.floor(x+dx) + 'px' + this.element.style.top = Math.floor(y+dy) + 'px' + } + if (hidden) + this.element.style.visibility = 'visible' + } +}) + + +/** + A Drawable is a CanvasNode with possible fill, stroke and clip. + + It draws the path by calling #drawGeometry + */ +Drawable = Klass(CanvasNode, { + pickable : true, + // 'inside' // clip before drawing the stroke + // | 'above' // draw stroke after the fill + // | 'below' // draw stroke before the fill + strokeMode : 'above', + + ABOVE : 'above', BELOW : 'below', INSIDE : 'inside', + + initialize : function(config) { + CanvasNode.initialize.call(this, config) + }, + + /** + Draws the picking path for the Drawable. + + The default version begins a new path and calls drawGeometry. + + @param ctx Canvas drawing context + */ + drawPickingPath : function(ctx) { + if (!this.drawGeometry) return + ctx.beginPath() + this.drawGeometry(ctx) + }, + + /** + Returns true if the point x,y is inside the path of a drawable node. + + The x,y point is in user-space coordinates, meaning that e.g. the point + 5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the + transform on the rectangle. + + @param x X-coordinate of the point. + @param y Y-coordinate of the point. + @return Whether the point is inside the path of this node. + @type boolean + */ + isPointInPath : function(x, y) { + return false + }, + + isVisible : function(ctx) { + var abb = this.getAxisAlignedBoundingBox() + if (!abb) return true + var x1 = abb[0], x2 = abb[0]+abb[2], y1 = abb[1], y2 = abb[1]+abb[3] + var w = this.root.width + var h = this.root.height + if (this.root.drawBoundingBoxes) { + ctx.save() + var bbox = this.getBoundingBox() + ctx.beginPath() + ctx.rect(bbox[0], bbox[1], bbox[2], bbox[3]) + ctx.strokeStyle = 'green' + ctx.lineWidth = 1 + ctx.stroke() + ctx.restore() + ctx.save() + CanvasSupport.setTransform(ctx, [1,0,0,1,0,0], this.currentMatrix) + ctx.beginPath() + ctx.rect(x1, y1, x2-x1, y2-y1) + ctx.strokeStyle = 'red' + ctx.lineWidth = 1.5 + ctx.stroke() + ctx.restore() + } + var visible = !(x2 < 0 || x1 > w || y2 < 0 || y1 > h) + return visible + }, + + createSubtreePath : function(ctx, skipTransform) { + ctx.save() + if (!skipTransform) this.transform(ctx, true) + if (this.drawGeometry) this.drawGeometry(ctx) + for (var i=0; i this.endAngle) { + ctx.lineTo(x+Math.cos(a)*r, y-Math.sin(a)*r) + a -= 0.1 + r = this.startRadius + this.radiusFunction(a) + } + } + a = this.endAngle + r = this.startRadius + this.radiusFunction(a) + ctx.lineTo(x+Math.cos(a)*r, y-Math.sin(a)*r) + }, + + isPointInPath : function(x, y) { + return false + } +}) + + +/** + Rectangle is used for creating rectangular paths. + + Uses context.rect(...). + + Attributes: + cx, cy, width, height, centered, rx, ry + + If centered is set to true, centers the rectangle on the origin. + Otherwise the top-left corner of the rectangle is on the origin. + + @param width Width of the rectangle. + @param height Height of the rectangle. + @param config Optional config hash. + */ +Rectangle = Klass(Drawable, { + cx : 0, + cy : 0, + x2 : 0, + y2 : 0, + width : 0, + height : 0, + rx : 0, + ry : 0, + centered : false, + + initialize : function(width, height, config) { + if (width != null) { + this.width = width + this.height = width + } + if (height != null) this.height = height + Drawable.initialize.call(this, config) + }, + + /** + Creates a rectangular path using ctx.rect(...). + + @param ctx Canvas drawing context. + */ + drawGeometry : function(ctx) { + var x = this.cx + var y = this.cy + var w = (this.width || (this.x2 - x)) + var h = (this.height || (this.y2 - y)) + if (w == 0 || h == 0) return + if (this.centered) { + x -= 0.5*w + y -= 0.5*h + } + if (this.rx || this.ry) { + // hahaa, welcome to the undocumented rounded corners path + // using bezier curves approximating ellipse quadrants + var rx = Math.min(w * 0.5, this.rx || this.ry) + var ry = Math.min(h * 0.5, this.ry || rx) + var k = 0.5522847498 + var krx = k*rx + var kry = k*ry + ctx.moveTo(x+rx, y) + ctx.lineTo(x-rx+w, y) + ctx.bezierCurveTo(x-rx+w + krx, y, x+w, y+ry-kry, x+w, y+ry) + ctx.lineTo(x+w, y+h-ry) + ctx.bezierCurveTo(x+w, y+h-ry+kry, x-rx+w+krx, y+h, x-rx+w, y+h) + ctx.lineTo(x+rx, y+h) + ctx.bezierCurveTo(x+rx-krx, y+h, x, y+h-ry+kry, x, y+h-ry) + ctx.lineTo(x, y+ry) + ctx.bezierCurveTo(x, y+ry-kry, x+rx-krx, y, x+rx, y) + ctx.closePath() + } else { + if (w < 0) x += w + if (h < 0) y += h + ctx.rect(x, y, Math.abs(w), Math.abs(h)) + } + }, + + /** + Returns true if the point x,y is inside this rectangle. + + The x,y point is in user-space coordinates, meaning that e.g. the point + 5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the + transform on the rectangle. + + @param x X-coordinate of the point. + @param y Y-coordinate of the point. + @return Whether the point is inside this rectangle. + @type boolean + */ + isPointInPath : function(x,y) { + x -= this.cx + y -= this.cy + if (this.centered) { + x += this.width/2 + y += this.height/2 + } + return (x >= 0 && x <= this.width && y >= 0 && y <= this.height) + }, + + getBoundingBox : function() { + var x = this.cx + var y = this.cy + if (this.centered) { + x -= this.width/2 + y -= this.height/2 + } + return [x,y,this.width,this.height] + } +}) + + +/** + Polygon is used for creating paths consisting of straight line + segments. + + Attributes: + segments - The vertices of the polygon, e.g. [0,0, 1,1, 1,2, 0,1] + closePath - Whether to close the path, default is true. + + @param segments The vertices of the polygon. + @param closePath Whether to close the path. + @param config Optional config hash. + */ +Polygon = Klass(Drawable, { + segments : [], + closePath : true, + + initialize : function(segments, config) { + this.segments = segments + Drawable.initialize.call(this, config) + }, + + drawGeometry : function(ctx) { + if (!this.segments || this.segments.length < 2) return + var s = this.segments + ctx.moveTo(s[0], s[1]) + for (var i=2; i= bbox[0] && px <= bbox[0]+bbox[2] && + py >= bbox[1] && py <= bbox[1]+bbox[3]) + }, + + getStartPoint : function() { + if (!this.segments || this.segments.length < 2) + return {point:[0,0], angle:0} + var a = 0 + if (this.segments.length > 2) { + a = Curves.lineAngle(this.segments.slice(0,2), this.segments.slice(2,4)) + } + return {point: this.segments.slice(0,2), + angle: a} + }, + + getEndPoint : function() { + if (!this.segments || this.segments.length < 2) + return {point:[0,0], angle:0} + var a = 0 + if (this.segments.length > 2) { + a = Curves.lineAngle(this.segments.slice(-4,-2), this.segments.slice(-2)) + } + return {point: this.segments.slice(-2), + angle: a} + }, + + getMidPoints : function() { + if (!this.segments || this.segments.length < 2) + return [] + var segs = this.segments + var verts = [] + for (var i=2; i maxX) maxX = x + if (y < minY) minY = y + if (y > maxY) maxY = y + } + return [minX, minY, maxX-minX, maxY-minY] + } +}) + + + +/** + CatmullRomSpline draws a Catmull-Rom spline, with optional looping and + path closing. Handy for motion paths. + + @param segments Control points for the spline, as [[x,y], [x,y], ...] + @param config Optional config hash. + */ +CatmullRomSpline = Klass(Drawable, { + segments : [], + loop : false, + closePath : false, + + initialize : function(segments, config) { + this.segments = segments + Drawable.initialize.call(this, config) + }, + + drawGeometry : function(ctx) { + var x1 = this.currentMatrix[0] + var x2 = this.currentMatrix[1] + var y1 = this.currentMatrix[2] + var y2 = this.currentMatrix[3] + var xs = x1*x1 + x2*x2 + var ys = y1*y1 + y2*y2 + var s = Math.floor(Math.sqrt(Math.max(xs, ys))) + var cmp = this.compiled + if (!cmp || cmp.scale != s) { + cmp = this.compile(s) + } + for (var i=0; i= (this.loop ? 1 : 4)) { + var segs = this.segments + if (this.loop) { + segs = segs.slice(0) + segs.unshift(segs[segs.length-1]) + segs.push(segs[1]) + segs.push(segs[2]) + } + // FIXME don't be stupid + var point_spacing = 1 / (15 * (scale+0.5)) + var a,b,c,d,p,pp + compiled.push(['moveTo', segs[1].slice(0)]) + p = segs[1] + for (var j=1; j maxX) maxX = x + if (y < minY) minY = y + if (y > maxY) maxY = y + } + } + return [minX, minY, maxX-minX, maxY-minY] + }, + + pointAt : function(t) { + if (!this.segments) return [0,0] + if (this.segments.length >= (this.loop ? 1 : 4)) { + var segs = this.segments + if (this.loop) { + segs = segs.slice(0) + segs.unshift(segs[segs.length-1]) + segs.push(segs[1]) + segs.push(segs[2]) + } + // turn t into segment_index.segment_t + var rt = t * (segs.length - 3) + var j = Math.floor(rt) + var st = rt-j + var a = segs[j], + b = segs[j+1], + c = segs[j+2], + d = segs[j+3] + return Curves.catmullRomPoint(a,b,c,d,st) + } else { + return this.segments[0] + } + }, + + pointAngleAt : function(t) { + if (!this.segments) return {point: [0,0], angle: 0} + if (this.segments.length >= (this.loop ? 1 : 4)) { + var segs = this.segments + if (this.loop) { + segs = segs.slice(0) + segs.unshift(segs[segs.length-1]) + segs.push(segs[1]) + segs.push(segs[2]) + } + // turn t into segment_index.segment_t + var rt = t * (segs.length - 3) + var j = Math.floor(rt) + var st = rt-j + var a = segs[j], + b = segs[j+1], + c = segs[j+2], + d = segs[j+3] + return Curves.catmullRomPointAngle(a,b,c,d,st) + } else { + return {point:this.segments[0] || [0,0], angle: 0} + } + } +}) + + +/** + Path is used for creating custom paths. + + Attributes: segments, closePath. + + var path = new Path([ + ['moveTo', [-50, -60]], + ['lineTo', [30, 50], + ['lineTo', [-50, 50]], + ['bezierCurveTo', [-50, 100, -50, 100, 0, 100]], + ['quadraticCurveTo', [0, 120, -20, 130]], + ['quadraticCurveTo', [0, 140, 0, 160]], + ['bezierCurveTo', [-10, 160, -20, 170, -30, 180]], + ['quadraticCurveTo', [10, 230, -50, 260]] + ]) + + The path segments are used as [methodName, arguments] on the canvas + drawing context, so the possible path segments are: + + ['moveTo', [x, y]] + ['lineTo', [x, y]] + ['quadraticCurveTo', [control_point_x, control_point_y, x, y]] + ['bezierCurveTo', [cp1x, cp1y, cp2x, cp2y, x, y]] + ['arc', [x, y, radius, startAngle, endAngle, drawClockwise]] + ['arcTo', [x1, y1, x2, y2, radius]] + ['rect', [x, y, width, height]] + + You can also pass an SVG path string as segments. + + var path = new Path("M 100 100 L 300 100 L 200 300 z", { + stroke: true, strokeStyle: 'blue', + fill: true, fillStyle: 'red', + lineWidth: 3 + }) + + @param segments The path segments. + @param config Optional config hash. + */ +Path = Klass(Drawable, { + segments : [], + closePath : false, + + initialize : function(segments, config) { + this.segments = segments + Drawable.initialize.call(this, config) + }, + + /** + Creates a path on the given drawing context. + + For each path segment, calls the context method named in the first element + of the segment with the rest of the segment elements as arguments. + + SVG paths are parsed and executed. + + Closes the path if closePath is true. + + @param ctx Canvas drawing context. + */ + drawGeometry : function(ctx) { + var segments = this.getSegments() + for (var i=0; i= bbox[0] && px <= bbox[0]+bbox[2] && + py >= bbox[1] && py <= bbox[1]+bbox[3]) + }, + + getBoundingBox : function() { + if (!(this.compiled && this.compiledBoundingBox)) { + var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity + var segments = this.getSegments() + for (var i=0; i maxX) maxX = x + if (y < minY) minY = y + if (y > maxY) maxY = y + } + } + this.compiledBoundingBox = [minX, minY, maxX-minX, maxY-minY] + } + return this.compiledBoundingBox + }, + + getStartPoint : function() { + var segs = this.getSegments() + if (!segs || !segs[0]) return {point: [0,0], angle: 0} + var fs = segs[0] + var c = fs[1] + var point = [c[c.length-2], c[c.length-1]] + var ss = segs[1] + var angle = 0 + if (ss) { + c2 = ss[1] + angle = Curves.lineAngle(point, [c2[c2.length-2], c2[c2.length-1]]) + } + return { + point: point, + angle: angle + } + }, + + getEndPoint : function() { + var segs = this.getSegments() + if (!segs || !segs[0]) return {point: [0,0], angle: 0} + var fs = segs[segs.length-1] + var c = fs[1] + var point = [c[c.length-2], c[c.length-1]] + var ss = segs[segs.length-2] + var angle = 0 + if (ss) { + c2 = ss[1] + angle = Curves.lineAngle([c2[c2.length-2], c2[c2.length-1]], point) + } + return { + point: point, + angle: angle + } + }, + + getMidPoints : function() { + var segs = this.getSegments() + if (this.vertices) + return this.vertices.slice(1,-1) + var verts = [] + for (var i=1; i 2) { + var a = segs[i-1][1].slice(-4,-2) + var t = 0.5 * (Curves.lineAngle(a,b) + Curves.lineAngle(b,c)) + } else { + var t = Curves.lineAngle(b,c) + } + verts.push( + {point: b, angle: t} + ) + var id = segs[i][2] + if (id != null) { + i++ + while (segs[i] && segs[i][2] == id) i++ + i-- + } + } + return verts + }, + + getSegments : function() { + if (typeof(this.segments) == 'string') { + if (!this.compiled || this.segments != this.compiledSegments) { + this.compiled = this.compileSVGPath(this.segments) + this.compiledSegments = this.segments + } + } else if (!this.compiled) { + this.compiled = Object.clone(this.segments) + } + return this.compiled + }, + + /** + Compiles an SVG path string into an array of canvas context method calls. + + Returns an array of [methodName, [arg1, arg2, ...]] method call arrays. + */ + compileSVGPath : function(svgPath) { + var segs = svgPath.split(/(?=[a-z])/i) + var x = 0 + var y = 0 + var px,py + var pc + var commands = [] + for (var i=0; i 1) { + pl = Math.sqrt(pl) + rx *= pl + ry *= pl + } + + var a00 = cos_th / rx + var a01 = sin_th / rx + var a10 = (-sin_th) / ry + var a11 = (cos_th) / ry + var x0 = a00 * ox + a01 * oy + var y0 = a10 * ox + a11 * oy + var x1 = a00 * x + a01 * y + var y1 = a10 * x + a11 * y + + var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) + var sfactor_sq = 1 / d - 0.25 + if (sfactor_sq < 0) sfactor_sq = 0 + var sfactor = Math.sqrt(sfactor_sq) + if (sweep == large) sfactor = -sfactor + var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0) + var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0) + + var th0 = Math.atan2(y0-yc, x0-xc) + var th1 = Math.atan2(y1-yc, x1-xc) + + var th_arc = th1-th0 + if (th_arc < 0 && sweep == 1){ + th_arc += 2*Math.PI + } else if (th_arc > 0 && sweep == 0) { + th_arc -= 2 * Math.PI + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))) + var result = [] + for (var i=0; i= 1) { + var rt = 1 + var seg = segments[segments.length-1] + } else if (config && config.discrete) { + var idx = Math.floor(t * segments.length) + var seg = segments[idx] + var rt = 0 + } else if (config && config.linear) { + var idx = t * segments.length + var rt = idx - Math.floor(idx) + var seg = segments[Math.floor(idx)] + } else { + var len = t * length + var rlen = 0, idx, rt + for (var i=0; i len) { + idx = i + rt = (len - rlen) / segments[i][2][3] + break + } + rlen += segments[i][2][3] + } + var seg = segments[idx] + } + var angle = 0 + var cmd = seg[2][0] + var args = seg[2][1] + switch (cmd) { + case 'bezierCurveTo': + return Curves.cubicLengthPointAngle([seg[0], seg[1]], [args[0], args[1]], [args[2], args[3]], [args[4], args[5]], rt) + break + case 'quadraticCurveTo': + return Curves.quadraticLengthPointAngle([seg[0], seg[1]], [args[0], args[1]], [args[2], args[3]], rt) + break + case 'lineTo': + x = Curves.linearValue(seg[0], args[0], rt) + y = Curves.linearValue(seg[1], args[1], rt) + angle = Curves.lineAngle([seg[0], seg[1]], [args[0], args[1]], rt) + break + } + return {point: [x, y], angle: angle } + } + +}) + + +/** + ImageNode is used for drawing images. Creates a rectangular path around + the drawn image. + + Attributes: + + centered - If true, image center is at the origin. + Otherwise image top-left is at the origin. + usePattern - Use a pattern fill for drawing the image (instead of + drawImage.) Doesn't do sub-image drawing, and Safari doesn't + like scaled image patterns. + sX, sY, sWidth, sHeight - Area of image to draw. Optional. + dX, dY - Coordinates where to draw the image. Default is 0, 0. + dWidth, dHeight - Size of the drawn image. Optional. + + Example: + + var img = new Image() + img.src = 'foo.jpg' + var imageGeo = new ImageNode(img) + + @param image Image to draw. + @param config Optional config hash. + */ +ImageNode = Klass(Drawable, { + centered : false, + usePattern : false, + + sX : 0, + sY : 0, + sWidth : null, + sHeight : null, + + dX : 0, + dY : 0, + dWidth : null, + dHeight : null, + + initialize : function(image, config) { + this.image = image + Drawable.initialize.call(this, config) + }, + + /** + Draws the image on the given drawing context. + + Creates a rectangular path around the drawn image (for possible stroke + and/or fill.) + + @param ctx Canvas drawing context. + */ + drawGeometry : function(ctx) { + if (Object.isImageLoaded(this.image)) { + var w = this.dWidth == null ? this.image.width : this.dWidth + var h = this.dHeight == null ? this.image.height : this.dHeight + var x = this.dX + (this.centered ? -w * 0.5 : 0) + var y = this.dY + (this.centered ? -h * 0.5 : 0) + if (this.dWidth != null) { + if (this.sWidth != null) { + ctx.drawImage(this.image, + this.sX, this.sY, this.sWidth, this.sHeight, + x, y, w, h) + } else { + ctx.drawImage(this.image, x, y, w, h) + } + } else { + w = this.image.width + h = this.image.height + if (this.usePattern) { + if (!this.imagePattern) + this.imagePattern = new Pattern(this.image, 'repeat') + var fs = this.imagePattern.compiled + if (!fs) + fs = this.imagePattern.compile(ctx) + ctx.save() + ctx.beginPath() + ctx.rect(x, y, w, h) + ctx.setFillStyle(fs) + ctx.fill() + ctx.restore() + ctx.beginPath() + } else { + ctx.drawImage(this.image, x, y) + } + } + } else { + var w = this.dWidth + var h = this.dHeight + if (!( w && h )) return + var x = this.dX + (this.centered ? -w * 0.5 : 0) + var y = this.dY + (this.centered ? -h * 0.5 : 0) + } + ctx.rect(x, y, w, h) + }, + + /** + Creates a bounding rectangle path for the image on the given drawing + context. + + @param ctx Canvas drawing context. + */ + drawPickingPath : function(ctx) { + var x = this.dX + (this.centered ? -this.image.width * 0.5 : 0) + var y = this.dY + (this.centered ? -this.image.height * 0.5 : 0) + var w = this.dWidth + var h = this.dHeight + if (this.dWidth == null) { + w = this.image.width + h = this.image.height + } + ctx.rect(x, y, w, h) + }, + + /** + Returns true if the point x,y is inside the image rectangle. + + The x,y point is in user-space coordinates, meaning that e.g. the point + 5,5 will always be inside the rectangle [0, 0, 10, 10], regardless of the + transform on the rectangle. + + @param x X-coordinate of the point. + @param y Y-coordinate of the point. + @return Whether the point is inside the image rectangle. + @type boolean + */ + isPointInPath : function(x,y) { + x -= this.dX + y -= this.dY + if (this.centered) { + x += this.image.width * 0.5 + y += this.image.height * 0.5 + } + var w = this.dWidth + var h = this.dHeight + if (this.dWidth == null) { + w = this.image.width + h = this.image.height + } + return ((x >= 0) && (x <= w) && (y >= 0) && (y <= h)) + }, + + getBoundingBox : function() { + x = this.dX + y = this.dY + if (this.centered) { + x -= this.image.width * 0.5 + y -= this.image.height * 0.5 + } + var w = this.dWidth + var h = this.dHeight + if (this.dWidth == null) { + w = this.image.width + h = this.image.height + } + return [x, y, w, h] + } +}) + +ImageNode.load = function(src) { + var img = new Image(); + img.src = src; + var imgn = new ImageNode(img); + return imgn; +} + + +/** + TextNode is used for drawing text on a canvas. + + Attributes: + + text - The text string to draw. + align - Horizontal alignment for the text. + 'left', 'right', 'center', 'start' or 'end' + baseline - Baseline used for the text. + 'top', 'hanging', 'middle', 'alphabetic', 'ideographic' or 'bottom' + asPath - If true, creates a text path instead of drawing the text. + pathGeometry - A geometry object the path of which the text follows. + + Example: + + var text = new TextGeometry('The cake is a lie.') + + @param text The text string to draw. + @param config Optional config hash. + */ +TextNode = Klass(Drawable, { + text : 'Text', + align : 'start', // 'left' | 'right' | 'center' | 'start' | 'end' + baseline : 'alphabetic', // 'top' | 'hanging' | 'middle' | 'alphabetic' | + // 'ideographic' | 'bottom' + accuratePicking : false, + asPath : false, + pathGeometry : null, + maxWidth : null, + width : 0, + height : 20, + cx : 0, + cy : 0, + + __drawMethodName : 'draw' + CanvasSupport.getTextBackend(), + __pickingMethodName : 'drawPickingPath' + CanvasSupport.getTextBackend(), + + initialize : function(text, config) { + this.lastText = this.text + this.text = text + Drawable.initialize.call(this, config) + }, + + drawGeometry : function(ctx) { + this.drawUsing(ctx, this.__drawMethodName) + }, + + drawPickingPath : function(ctx) { + this.drawUsing(ctx, this.__pickingMethodName) + }, + + drawUsing : function(ctx, methodName) { + if (!this.text || this.text.length == 0) + return + if (this.lastText != this.text || this.lastStyle != ctx.font) { + this.dimensions = this.measureText(ctx) + this.lastText = this.text + this.lastStyle = ctx.font + } + if (this[methodName]) + this[methodName](ctx) + }, + + measureText : function(ctx) { + var mn = 'measureText' + CanvasSupport.getTextBackend().capitalize() + if (this[mn]) { + return this[mn](ctx) + } else { + return {width: 0, height: 0} + } + }, + + computeXForAlign : function() { + if (this.align == 'left') // most hit branch + return 0 + else if (this.align == 'right') + return -this.dimensions.width + else if (this.align == 'center') + return -this.dimensions.width * 0.5 + }, + + measureTextHTML5 : function(ctx) { + // FIXME measureText is retarded + return {width: ctx.measureText(this.text).width, height: 20} + }, + + drawHTML5 : function(ctx) { + ctx.fillText(this.text, this.cx, this.cy, this.maxWidth) + }, + + drawPickingPathHTML5 : function(ctx) { + var ascender = 15 // this.dimensions.ascender + var ry = this.cy - ascender + ctx.rect(this.cx, ry, this.dimensions.width, this.dimensions.height) + }, + + measureTextMozText : function(ctx) { + return {width: ctx.mozMeasureText(this.text), height: 20} + }, + + drawMozText : function(ctx) { + var x = this.cx + this.computeXForAlign() + var y = this.cy + 0 + if (this.pathGeometry) { + this.pathGeometry.draw(ctx) + ctx.mozDrawTextAlongPath(this.text, this.path) + } else { + ctx.save() + ctx.translate(x,y) + if (this.asPath) { + ctx.mozPathText(this.text) + } else { + ctx.mozDrawText(this.text) + } + ctx.restore() + } + }, + + drawPickingPathMozText : function(ctx) { + var x = this.cx + this.computeXForAlign() + var y = this.cy + 0 + if (this.pathGeometry) { // FIXME how to draw a text path along path? + this.pathGeometry.draw(ctx) + // ctx.mozDrawTextAlongPath(this.text, this.path) + } else if (!this.accuratePicking) { + var ascender = 15 // this.dimensions.ascender + var ry = y - ascender + ctx.rect(x, ry, this.dimensions.width, this.dimensions.height) + } else { + ctx.save() + ctx.translate(x,y) + ctx.mozPathText(this.text) + ctx.restore() + } + }, + + drawDrawString : function(ctx) { + var x = this.cx + this.computeXForAlign() + var y = this.cy + 0 + ctx.drawString(x,y, this.text) + }, + + measureTextPerfectWorld : function(ctx) { + return ctx.measureText(this.text) + }, + + drawPerfectWorld : function(ctx) { + if (this.pathGeometry) { + this.pathGeometry.draw(ctx) + if (this.asPath) + ctx.pathTextAlongPath(this.text) + else + ctx.drawTextAlongPath(this.text) + } else if (this.asPath) { + ctx.pathText(this.text) + } else { + ctx.drawText(this.text) + } + }, + + drawPickingPathPerfectWorld : function(ctx) { + if (this.accuratePicking) { + if (this.pathGeometry) { + ctx.pathTextAlongPath(this.text) + } else { + ctx.pathText(this.text) + } + } else { // creates a path of text bounding box + if (this.pathGeometry) { + ctx.textRectAlongPath(this.text) + } else { + ctx.textRect(this.text) + } + } + } +}) + + +/** + Gradient is a linear or radial color gradient that can be used as a + strokeStyle or fillStyle. + + Attributes: + + type - Type of the gradient. 'linear' or 'radial' + startX, startY - Coordinates for the starting point of the gradient. + Center of the starting circle of a radial gradient. + Default is 0, 0. + endX, endY - Coordinates for the ending point of the gradient. + Center of the ending circle of a radial gradient. + Default is 0, 0. + startRadius - The radius of the starting circle of a radial gradient. + Default is 0. + endRadius - The radius of the ending circle of a radial gradient. + Default is 100. + colorStops - The color stops for the gradient. The format for the color + stops is: [[position_1, color_1], [position_2, color_2], ...]. + The possible color formats are: 'red', '#000', '#000000', + 'rgba(0,0,0, 0.2)', [0,0,0] and [0,0,0, 0.2]. + Default color stops are [[0, '#000000'], [1, '#FFFFFF']]. + + Example: + + var g = new Gradient({ + type : 'radial', + endRadius : 40, + colorStops : [ + [0, '#000'], + [0.2, '#ffffff'], + [0.5, [255, 0, 0]], + [0.8, [0, 255, 255, 0.5]], + [1.0, 'rgba(255, 0, 255, 0.8)'] + ] + }) + + @param config Optional config hash. + */ +Gradient = Klass({ + type : 'linear', + isPattern : true, + startX : 0, + startY : 0, + endX : 1, + endY : 0, + startRadius : 0, + endRadius : 1, + colorStops : [], + + initialize : function(config) { + this.colorStops = [[0, '#000000'], [1, '#FFFFFF']] + if (config) Object.extend(this, config) + }, + + /** + Compiles the gradient using the given drawing context. + Returns a gradient object that can be used as drawing context + fill/strokeStyle. + + @param ctx Drawing context to compile pattern on. + @return Gradient object. + */ + compile : function(ctx) { + if (this.type == 'linear') { + var go = ctx.createLinearGradient( + this.startX, this.startY, + this.endX, this.endY) + } else { + var go = ctx.createRadialGradient( + this.startX, this.startY, this.startRadius, + this.endX, this.endY, this.endRadius) + } + for(var i=0; i width and height, viewBox clipping. + * Clipping (objectBoundingBox clipping too) + * Paths, rectangles, ellipses, circles, lines, polylines and polygons + * Simple untransformed text using HTML + * Nested transforms + * Transform lists (transform="rotate(30) translate(2,2) scale(4)") + * Gradient and pattern transforms + * Strokes with miter, joins and caps + * Flat fills and gradient fills, ditto for strokes + * Parsing simple stylesheets (tag, class or id) + * Images + * Non-pixel units (cm, mm, in, pt, pc, em, ex, %) + * -tags + * preserveAspectRatio + * Dynamic gradient sizes (objectBoundingBox, etc.) + * Markers (though buggy) + + Some of the several missing features: + * Masks + * Patterns + * viewBox clipping for elements other than and + * Text styling + * tspan, tref, textPath, many things text + * Fancy style rules (tag .class + #foo > bob#alice { ... }) + * Filters + * Animation + * Dashed strokes + */ +SVGParser = { + /** + Loads an SVG document using XMLHttpRequest and calls the given onSuccess + callback with the parsed document. If loading fails, calls onFailure + instead if one is given. When loading and an onLoading callback is given, + calls onLoading every time xhr.readyState is 3. + + The callbacks will be called with the following parameters: + + onSuccess(svgNode, xmlHttpRequest, + filename, config) + + onFailure(xmlHttpRequest, possibleException, + filename, config) + + onLoading(xmlHttpRequest, + filename, config) + + Config hash parameters: + filename: Filename for the SVG document. Used for parsing image paths. + width: Width of the bounding box to fit the SVG in. + height: Height of the bounding box to fit the SVG in. + fontSize: Default font size for the SVG document. + onSuccess: Function to call on successful load. Required. + onFailure: Function to call on failed load. + onLoading: Function to call while loading. + + @param config The config hash. + @param filename The URL of the SVG document to load. Must conform to SOP. + */ + load : function(filename, config) { + if (!config.onSuccess) throw("Need to provide an onSuccess function.") + if (!config.filename) + config.filename = filename + var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : + new ActiveXObject("MSXML2.XMLHTTP.3.0") + xhr.open('GET', filename, true) + xhr.overrideMimeType('text/xml') + var failureFired = false + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200 || xhr.status == 0) { + try { + var svg = xhr.responseXML + var svgNode = SVGParser.parse(svg, config) + svgNode.svgRootElement = svg + } catch(e) { + if (config.onFailure) + config.onFailure(xhr, e, filename, config) + return + } + config.onSuccess(svgNode, xhr, filename, config) + } else { + if (config.onFailure && !failureFired) + config.onFailure(xhr, null, filename, config) + } + } else if (xhr.readyState == 3) { + if (config.onLoading) + config.onLoading(xhr, filename, config) + } + } + try { + xhr.send(null) + } catch(e) { + if (config.onFailure) { + config.onFailure(xhr, e, filename, config) + failureFired = true + } + xhr.abort() + } + }, + + /** + Parses an SVG DOM into CAKE scenegraph. + + Config hash parameters: + filename: Filename for the SVG document. Used for parsing image paths. + width: Width of the bounding box to fit the SVG in. + height: Height of the bounding box to fit the SVG in. + fontSize: Default font size for the SVG document. + currentColor: HTML text color of the containing element. + + @param svgRootElement The root element of the SVG DOM. + @param config The config hash. + @returns The root CanvasNode of the scenegraph created from the SVG document. + @type CanvasNode + */ + parse : function(svgRootElement, config) { + var n = new CanvasNode() + var w = config.width, h = config.height, fs = config.fontSize + n.innerWidth = w || window.innerWidth + n.innerHeight = h || window.innerHeight + n.innerSize = Math.sqrt(w*w + h*h) / Math.sqrt(2) + n.fontSize = fs || 12 + n.filename = config.filename || '.' + var defs = {} + var style = { ids : {}, classes : {}, tags : {} } + n.color = config.currentColor || 'black' + this.parseChildren(svgRootElement, n, defs, style) + n.defs = defs + n.style = style + return n + }, + + parsePreserveAspectRatio : function(aspect, w, h, vpw, vph) { + var aspect = aspect || "" + var aspa = aspect.split(/\s+/) + var defer = (aspa[0] == 'defer') + if (defer) aspa.shift() + var align = (aspa[0] || 'xMidYMid') + var meet = (aspa[1] || 'meet') + var wf = w / vpw + var hf = h / vph + var xywh = {x:0, y:0, w:wf, h:hf} + if (align == 'none') return xywh + xywh.w = xywh.h = (meet == 'meet' ? Math.min : Math.max)(wf, hf) + var xa = align.slice(1, 4).toLowerCase() + var ya = align.slice(5, 8).toLowerCase() + var xf = (this.SVGAlignMap[xa] || 0) + var yf = (this.SVGAlignMap[ya] || 0) + xywh.x = xf * (w-vpw*xywh.w) + xywh.y = yf * (h-vph*xywh.h) + return xywh + }, + + SVGAlignMap : { + min : 0, + mid : 0.5, + max : 1 + }, + + SVGTagMapping : { + svg : function(c, cn, defs, style) { + var p = new Rectangle() + p.width = 0 + p.height = 0 + p.doFill = function(){} + p.doStroke = function(){} + p.drawMarkers = function(){} + p.fill = 'black' + p.stroke = 'none' + var vb = c.getAttribute('viewBox') + var w = c.getAttribute('width') + var h = c.getAttribute('height') + if (!w) w = h + else if (!h) h = w + if (w) { + var wpx = this.parseUnit(w, cn, 'x') + var hpx = this.parseUnit(h, cn, 'y') + } + if (vb) { + xywh = vb.match(/[-+]?\d+/g).map(parseFloat) + p.cx = xywh[0] + p.cy = xywh[1] + p.width = xywh[2] + p.height = xywh[3] + var iw = cn.innerWidth = p.width + var ih = cn.innerHeight = p.height + cn.innerSize = Math.sqrt(iw*iw + ih*ih) / Math.sqrt(2) + if (c.getAttribute('overflow') != 'visible') + p.clip = true + } + if (w) { + if (vb) { // nuts, let's parse the alignment :| + var aspect = c.getAttribute('preserveAspectRatio') + var align = this.parsePreserveAspectRatio(aspect, + wpx, hpx, + p.width, p.height) + p.cx -= align.x / align.w + p.cy -= align.y / align.h + p.width = wpx / align.w + p.height = hpx / align.h + p.x += align.x + p.y += align.y + p.scale = [align.w, align.h] + } + // wrong place! + cn.docWidth = wpx + cn.docHeight = hpx + } + return p + }, + + marker : function(c, cn) { + var p = new CanvasNode() + p.draw = function(ctx) { + if (this.overflow != 'hidden' && this.viewBox) return + ctx.beginPath() + ctx.rect( + this.viewBox[0],this.viewBox[1], + this.viewBox[2],this.viewBox[3] + ) + ctx.clip() + } + var x = -this.parseUnit(c.getAttribute('refX'), cn, 'x') || 0 + var y = -this.parseUnit(c.getAttribute('refY'), cn, 'y') || 0 + p.transformList = [['translate', [x,y]]] + p.markerUnits = c.getAttribute('markerUnits') || 'strokeWidth' + p.markerWidth = this.parseUnit(c.getAttribute('markerWidth'), cn, 'x') || 3 + p.markerHeight = this.parseUnit( + c.getAttribute('markerHeight'), cn, 'y') || + 3 + p.overflow = c.getAttribute('overflow') || 'hidden' + p.viewBox = c.getAttribute('viewBox') + p.orient = c.getAttribute('orient') || 0 + if (p.orient && p.orient != 'auto') + p.orient = parseFloat(p.orient)*SVGMapping.DEG_TO_RAD_FACTOR + if (p.viewBox) { + p.viewBox = p.viewBox.strip().split(/[\s,]+/g).map(parseFloat) + var vbw = p.viewBox[2] - p.viewBox[0] + var vbh = p.viewBox[3] - p.viewBox[1] + if (p.markerWidth) { + var sx = sy = Math.min( + p.markerWidth / vbw, + p.markerHeight / vbh + ) + p.transformList.unshift(['scale', [sx,sy]]) + } + } + return p + }, + + clipPath : function(c,cn) { + var p = new CanvasNode() + p.units = c.getAttribute('clipPathUnits') + return p + }, + + title : function(c, canvasNode) { + canvasNode.root.title = c.textContent + }, + + desc : function(c,cn) { + cn.root.description = c.textContent + }, + + metadata : function(c, cn) { + cn.root.metadata = c + }, + + + + + + + + + parseAnimateTag : function(c, cn) { + var after = SVGParser.SVGTagMapping.parseTime(c.getAttribute('begin')) + var dur = SVGParser.SVGTagMapping.parseTime(c.getAttribute('dur')) + var end = SVGParser.SVGTagMapping.parseTime(c.getAttribute('end')) + if (dur == null) dur = end-after + dur = isNaN(dur) ? 0 : dur + var variable = c.getAttribute('attributeName') + var fill = c.getAttribute('fill') + if (cn.tagName == 'rect') { + if (variable == 'x') variable = 'cx' + if (variable == 'y') variable = 'cy' + } + var accum = c.getAttribute('accumulate') == 'sum' + var additive = c.getAttribute('additive') + if (additive) additive = additive == 'sum' + else additive = accum + var repeat = c.getAttribute('repeatCount') + if (repeat == 'indefinite') repeat = true + else repeat = parseFloat(repeat) + if (!repeat && dur > 0) { + var repeatDur = c.getAttribute('repeatDur') + if (repeatDur == 'indefinite') repeat = true + else repeat = SVGParser.SVGTagMapping.parseTime(repeatDur) / dur + } + return { + after: isNaN(after) ? 0 : after, + duration: dur, + restart: c.getAttribute('restart'), + calcMode : c.getAttribute('calcMode'), + additive : additive, + accumulate : accum, + repeat : repeat, + variable: variable, + fill: fill + } + }, + + parseTime : function(value) { + if (!value) return null + if (value.match(/[0-9]$/)) { + var hms = value.split(":") + var s = hms[hms.length-1] || 0 + var m = hms[hms.length-2] || 0 + var h = hms[hms.length-3] || 0 + return (parseFloat(h)*3600 + parseFloat(m)*60 + parseFloat(s)) * 1000 + } else { + var fac = 60 + if (value.match(/s$/i)) fac = 1 + else if (value.match(/h$/i)) fac = 3600 + return parseFloat(value) * fac * 1000 + } + }, + + + + + + + animate : function(c, cn) { + var from = this.parseUnit(c.getAttribute('from'), cn, 'x') + var to = this.parseUnit(c.getAttribute('to'), cn, 'x') + var by = this.parseUnit(c.getAttribute('by'), cn, 'x') + var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn) + if (c.getAttribute('values')) { + var self = this + var vals = c.getAttribute('values') + vals = vals.split(";").map(function(v) { + var xy = v.split(/[, ]+/) + if (xy.length > 2) { + return xy.map(function(x){ return self.parseUnit(x, cn, 'x') }) + } else if (xy.length > 1) { + return [ + self.parseUnit(xy[0], cn, 'x'), + self.parseUnit(xy[1], cn, 'y') + ] + } else { + return self.parseUnit(v, cn, 'x') + } + }) + } else { + if (to == null) to = from + by + } + cn.after(o.after, function() { + if (o.fill == 'remove') { + var orig = Object.clone(this[o.variable]) + this.after(o.duration, function(){ this[o.variable] = orig }) + } + if (vals) { + if (o.additive) { + var ov = this[o.variable] + vals = vals.map(function(v){ + return Object.sum(v, ov) + }) + } + var length = 0 + var lens = [] + if (vals[0] instanceof Array) { + for (var i=1; i len) { + idx = i + rt = (len - rlen) / lens[i] + break + } + rlen += lens[i] + } + var v0 = idx + var v1 = v0 + 1 + } else { + var idx = pos * (vals.length-1) + var v0 = Math.floor(idx) + var rt = idx - v0 + var v1 = v0 + 1 + } + this.tweenVariable(o.variable, vals[v0], vals[v1], rt, o.calcMode) + } + } + this.animate(animator, from, to, o.duration, 'linear', { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + } else { + if (from == null) { + from = this[o.variable] + if (by != null) to = from + by + } + if (o.additive) { + from = Object.sum(from, this[o.variable]) + to = Object.sum(to, this[o.variable]) + } + this.animate(o.variable, from, to, o.duration, o.calcMode, { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + } + }) + }, + + set : function(c, cn) { + var to = c.getAttribute('to') + var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn) + cn.after(o.after, function() { + if (o.fill == 'remove') { + var orig = Object.clone(this[o.variable]) + this.after(o.duration, function(){ this[o.variable] = orig }) + } + this[o.variable] = to + }) + }, + + animateMotion : function(c,cn) { + var path + if (c.getAttribute('path')) { + path = new Path(c.getAttribute('path')) + } else if (c.getAttribute('values')) { + var vals = c.getAttribute('values') + path = new Path("M" + vals.split(";").join("L")) + } else if (c.getAttribute('from') || c.getAttribute('to') || c.getAttribute('by')) { + var from = c.getAttribute('from') + var to = c.getAttribute('to') + var by = c.getAttribute('by') + if (!from) from = "0,0" + if (!to) to = "l" + by + else to = "L" + to + path = new Path("M" + from + to) + } + var p = new CanvasNode() + p.__motionPath = path + var rotate = c.getAttribute('rotate') + var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn) + cn.after(o.after, function() { + if (o.fill == 'remove') { + var ox = this.x, oy = this.y + this.after(o.duration, function(){ this.x = ox; this.y = oy}) + } + var motion = function(pos) { + var pa = p.__motionPath.pointAngleAt(pos, { + discrete: o.calcMode == 'discrete', + linear : o.calcMode == 'linear' + }) + this.x = pa.point[0] + this.y = pa.point[1] + if (rotate == 'auto') { + this.rotation = pa.angle + } else if (rotate == 'auto-reverse') { + this.rotation = pa.angle + Math.PI + } + } + this.animate(motion, 0, 1, o.duration, 'linear', { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + }) + return p + }, + + mpath : function(c,cn, defs) { + var href = c.getAttribute('xlink:href') + href = href.replace(/^#/,'') + this.getDef(defs, href, function(obj) { + cn.__motionPath = obj + }) + }, + + animateColor : function(c, cn, defs) { + var from = c.getAttribute('from') + var to = c.getAttribute('to') + from = SVGParser.SVGMapping.__parseStyle(from, null, defs) + to = SVGParser.SVGMapping.__parseStyle(to, null, defs) + var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn) + cn.after(o.after, function() { + if (o.fill == 'remove') { + var orig = Object.clone(this[o.variable]) + this.after(o.duration, function(){ this[o.variable] = orig }) + } + this.animate(o.variable, from, to, o.duration, 'linear', { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + }) + }, + + animateTransform : function(c, cn) { + var from = c.getAttribute('from') + var to = c.getAttribute('to') + var by = c.getAttribute('by') + var o = SVGParser.SVGTagMapping.parseAnimateTag(c, cn) + if (from) from = from.split(/[ ,]+/).map(parseFloat) + if (to) to = to.split(/[ ,]+/).map(parseFloat) + if (by) by = by.split(/[ ,]+/).map(parseFloat) + o.variable = c.getAttribute('type') + if (o.variable == 'rotate') { + o.variable = 'rotation' + if (from) from = from.map(function(v) { return v * Math.PI/180 }) + if (to) to = to.map(function(v) { return v * Math.PI/180 }) + if (by) by = by.map(function(v) { return v * Math.PI/180 }) + } else if (o.variable.match(/^skew/)) { + if (from) from = from.map(function(v) { return v * Math.PI/180 }) + if (to) to = to.map(function(v) { return v * Math.PI/180 }) + if (by) by = by.map(function(v) { return v * Math.PI/180 }) + } + if (to == null) to = Object.sum(from, by) + cn.after(o.after, function() { + if (o.variable == 'translate') { + if (from == null) { + from = [this.x, this.y] + if (by != null) to = Object.sum(from, by) + } + if (o.fill == 'remove') { + var ox = this.x + var oy = this.y + this.after(o.duration, function(){ this.x = ox; this.y = oy }) + } + this.animate('x', from[0], to[0], o.duration, 'linear', { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + if (from[1] != null) { + this.animate('y', from[1], to[1], o.duration, 'linear', { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + } + } else { + if (from) { + if (from.length == 1) from = from[0] + } + if (to) { + if (to.length == 1) to = to[0] + } + if (by) { + if (by.length == 1) by = by[0] + } + if (from == null) { + from = this[o.variable] + if (by != null) to = Object.sum(from, by) + } + if (o.variable == 'scale' && o.additive) { + // +1 in SMIL's additive scale means *1, welcome to brokenville + o.additive = false + } + if (o.fill == 'remove') { + var orig = Object.clone(this[o.variable]) + this.after(o.duration, function(){ this[o.variable] = orig }) + } + this.animate(o.variable, from, to, o.duration, 'linear', { + repeat: o.repeat, + additive: o.additive, + accumulate: o.accumulate + }) + } + }) + }, + + a : function(c, cn) { + var href = c.getAttribute('xlink:href') || + c.getAttribute('href') + var target = c.getAttribute('target') + var p = new LinkNode(href, target) + return p + }, + + use : function(c, cn, defs, style) { + var id = c.getAttribute('xlink:href') || + c.getAttribute('href') + var p = new CanvasNode() + if (id) { + id = id.replace(/^#/,'') + this.getDef(defs, id, function(obj) { + var oc = obj.clone() + var par = p.parent + if (par) { + if (p.stroke) oc.stroke = p.stroke + if (p.fill) oc.fill = p.fill + p.append(oc) + } else { + p = oc + } + }) + } + return p + }, + + image : function(c, cn, defs, style) { + var src = c.getAttribute('xlink:href') || + c.getAttribute('href') + if (src && src.search(/^[a-z]+:/i) != 0) { + src = cn.root.filename.split("/").slice(0,-1).join("/") + "/" + src + } + var p = new ImageNode(src ? Object.loadImage(src) : null) + p.fill = 'none' + p.dX = this.parseUnit(c.getAttribute('x'), cn, 'x') || 0 + p.dY = this.parseUnit(c.getAttribute('y'), cn, 'y') || 0 + p.srcWidth = this.parseUnit(c.getAttribute('width'), cn, 'x') + p.srcHeight = this.parseUnit(c.getAttribute('height'), cn, 'y') + return p + }, + + path : function(c) { + return new Path(c.getAttribute("d")) + }, + + polygon : function(c) { + return new Polygon(c.getAttribute("points").toString().strip() + .split(/[\s,]+/).map(parseFloat)) + }, + + polyline : function(c) { + return new Polygon(c.getAttribute("points").toString().strip() + .split(/[\s,]+/).map(parseFloat), {closePath:false}) + }, + + rect : function(c, cn) { + var p = new Rectangle( + this.parseUnit(c.getAttribute('width'), cn, 'x'), + this.parseUnit(c.getAttribute('height'), cn, 'y') + ) + p.cx = this.parseUnit(c.getAttribute('x'), cn, 'x') || 0 + p.cy = this.parseUnit(c.getAttribute('y'), cn, 'y') || 0 + p.rx = this.parseUnit(c.getAttribute('rx'), cn, 'x') || 0 + p.ry = this.parseUnit(c.getAttribute('ry'), cn, 'y') || 0 + return p + }, + + line : function(c, cn) { + var x1 = this.parseUnit(c.getAttribute('x1'), cn, 'x') || 0 + var y1 = this.parseUnit(c.getAttribute('y1'), cn, 'y') || 0 + var x2 = this.parseUnit(c.getAttribute('x2'), cn, 'x') || 0 + var y2 = this.parseUnit(c.getAttribute('y2'), cn, 'y') || 0 + var p = new Line(x1,y1, x2,y2) + return p + }, + + circle : function(c, cn) { + var p = new Circle(this.parseUnit(c.getAttribute('r'), cn) || 0) + p.cx = this.parseUnit(c.getAttribute('cx'), cn, 'x') || 0 + p.cy = this.parseUnit(c.getAttribute('cy'), cn, 'y') || 0 + return p + }, + + ellipse : function(c, cn) { + var p = new Ellipse( + this.parseUnit(c.getAttribute('rx'), cn, 'x') || 0, + this.parseUnit(c.getAttribute('ry'), cn, 'y') || 0 + ) + p.cx = this.parseUnit(c.getAttribute('cx'), cn, 'x') || 0 + p.cy = this.parseUnit(c.getAttribute('cy'), cn, 'y') || 0 + return p + }, + + text : function(c, cn) { + if (false) { + var p = new TextNode(c.textContent.strip()) + p.setAsPath(true) + p.cx = this.parseUnit(c.getAttribute('x'),cn, 'x') || 0 + p.cy = this.parseUnit(c.getAttribute('y'),cn, 'y') || 0 + return p + } else { + var e = E('div', c.textContent.strip()) + e.style.marginTop = '-1em' + e.style.whiteSpace = 'nowrap' + var p = new ElementNode(e) + p.xOffset = this.parseUnit(c.getAttribute('x'),cn, 'x') || 0 + p.yOffset = this.parseUnit(c.getAttribute('y'),cn, 'y') || 0 + return p + } + }, + + style : function(c, cn, defs, style) { + this.parseStyle(c, style) + }, + + defs : function(c, cn, defs, style) { + return new CanvasNode({visible: false}) + }, + + linearGradient : function(c, cn,defs,style) { + var g = new Gradient({type:'linear'}) + g.color = cn.color + if (c.getAttribute('color')) { + SVGParser.SVGMapping.color(g, c.getAttribute('color'), defs, style) + } + g.svgNode = c + var x1 = c.getAttribute('x1') + var y1 = c.getAttribute('y1') + var x2 = c.getAttribute('x2') + var y2 = c.getAttribute('y2') + var transform = c.getAttribute('gradientTransform') + g.units = c.getAttribute('gradientUnits') || "objectBoundingBox" + if (x1) g.startX = parseFloat(x1) * (x1.charAt(x1.length-1) == '%' ? 0.01 : 1) + if (y1) g.startY = parseFloat(y1) * (y1.charAt(y1.length-1) == '%' ? 0.01 : 1) + if (x2) g.endX = parseFloat(x2) * (x2.charAt(x2.length-1) == '%' ? 0.01 : 1) + if (y2) g.endY = parseFloat(y2) * (y2.charAt(y2.length-1) == '%' ? 0.01 : 1) + if (transform) this.applySVGTransform(g, transform, defs, style) + this.parseStops(g, c, defs, style) + return g + }, + + radialGradient : function(c, cn, defs, style) { + var g = new Gradient({type:'radial'}) + g.color = cn.color + if (c.getAttribute('color')) { + SVGParser.SVGMapping.color(g, c.getAttribute('color'), defs, style) + } + g.svgNode = c + var r = c.getAttribute('r') + var fx = c.getAttribute('fx') + var fy = c.getAttribute('fy') + var cx = c.getAttribute('cx') + var cy = c.getAttribute('cy') + var transform = c.getAttribute('gradientTransform') + g.units = c.getAttribute('gradientUnits') || "objectBoundingBox" + if (r) g.endRadius = parseFloat(r) * (r.charAt(r.length-1) == '%' ? 0.01 : 1) + if (fx) g.startX = parseFloat(fx) * (fx.charAt(fx.length-1) == '%' ? 0.01 : 1) + if (fy) g.startY = parseFloat(fy) * (fy.charAt(fy.length-1) == '%' ? 0.01 : 1) + if (cx) g.endX = parseFloat(cx) * (cx.charAt(cx.length-1) == '%' ? 0.01 : 1) + if (cy) g.endY = parseFloat(cy) * (cy.charAt(cy.length-1) == '%' ? 0.01 : 1) + if (transform) this.applySVGTransform(g, transform, defs, style) + this.parseStops(g, c, defs, style) + return g + } + }, + + parseChildren : function(node, canvasNode, defs, style) { + var childNodes = [] + var cn = canvasNode + for (var i=0; i 0) + g.colorStops = stops + }, + + applySVGTransform : function(node, transform, defs, style) { + if (!transform) return + node.transformList = [] + var segs = transform.match(/[a-z]+\s*\([^)]*\)/ig) + for (var i=0; i 1) + node.transformList.push(['rotate', [angle, rot[1