From 9354f69ddac5b9e1c1b3d8e47b4367540606efd2 Mon Sep 17 00:00:00 2001 From: dsc Date: Sat, 30 Oct 2010 16:54:53 -0700 Subject: [PATCH 1/1] 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