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 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], rot[2] || 0]]) + else + node.transformList.push(['rotate', [angle]]) + }, + + scale : function(node, v) { + var xy = v.split(/[\s,]+/).map(parseFloat) + var trans = ['scale'] + if (xy.length > 1) + trans[1] = [xy[0], xy[1]] + else + trans[1] = [xy[0], xy[0]] + node.transformList.push(trans) + }, + + matrix : function(node, v) { + var mat = v.split(/[\s,]+/).map(parseFloat) + node.transformList.push(['matrix', mat]) + }, + + skewX : function(node, v) { + var angle = parseFloat(v)*this.DEG_TO_RAD_FACTOR + node.transformList.push(['skewX', [angle]]) + }, + + skewY : function(node, v) { + var angle = parseFloat(v)*this.DEG_TO_RAD_FACTOR + node.transformList.push(['skewY', [angle]]) + }, + + opacity : function(node, v) { + node.opacity = parseFloat(v) + }, + + display : function (node, v) { + node.display = v + }, + + visibility : function (node, v) { + node.visibility = v + }, + + 'stroke-miterlimit' : function(node, v) { + node.miterLimit = parseFloat(v) + }, + + 'stroke-linecap' : function(node, v) { + node.lineCap = v + }, + + 'stroke-linejoin' : function(node, v) { + node.lineJoin = v + }, + + 'stroke-width' : function(node, v) { + node.strokeWidth = this.parseUnit(v, node) + }, + + fill : function(node, v, defs, style) { + node.fill = this.__parseStyle(v, node.fill, defs, node.color) + }, + + stroke : function(node, v, defs, style) { + node.stroke = this.__parseStyle(v, node.stroke, defs, node.color) + }, + + color : function(node, v, defs, style) { + if (v == 'inherit') return + node.color = this.__parseStyle(v, false, defs, node.color) + }, + + 'stop-color' : function(node, v, defs, style) { + if (v == 'none') { + node[1] = [0,0,0,0] + } else { + node[1] = this.__parseStyle(v, node[1], defs, node.color) + } + }, + + 'fill-opacity' : function(node, v) { + node.fillOpacity = Math.min(1,Math.max(0,parseFloat(v))) + }, + + 'stroke-opacity' : function(node, v) { + node.strokeOpacity = Math.min(1,Math.max(0,parseFloat(v))) + }, + + 'stop-opacity' : function(node, v) { + node[1] = node[1] || [0,0,0] + node[1][3] = Math.min(1,Math.max(0,parseFloat(v))) + }, + + 'text-anchor' : function(node, v) { + node.textAnchor = v + if (node.setAlign) { + if (v == 'middle') + node.setAlign('center') + else + node.setAlign(v) + } + }, + + 'font-family' : function(node, v) { + node.fontFamily = v + }, + + 'font-size' : function(node, v) { + node.fontSize = this.parseUnit(v, node) + }, + + __parseStyle : function(v, currentStyle, defs, currentColor) { + + if (v.charAt(0) == '#') { + if (v.length == 4) + v = v.replace(/([^#])/g, '$1$1') + var a = v.slice(1).match(/../g).map( + function(i) { return parseInt(i, 16) }) + return a + + } else if (v.search(/^rgb\(/) != -1) { + var a = v.slice(4,-1).split(",") + for (var i=0; i elements as element.getContext(). + * @this {HTMLElement} + * @return {CanvasRenderingContext2D_} + */ + function getContext() { + return this.context_ || + (this.context_ = new CanvasRenderingContext2D_(this)); + } + + var slice = Array.prototype.slice; + + /** + * Binds a function to an object. The returned function will always use the + * passed in {@code obj} as {@code this}. + * + * Example: + * + * g = bind(f, obj, a, b) + * g(c, d) // will do f.call(obj, a, b, c, d) + * + * @param {Function} f The function to bind the object to + * @param {Object} obj The object that should act as this when the function + * is called + * @param {*} var_args Rest arguments that will be used as the initial + * arguments when the function is called + * @return {Function} A new function that has bound this + */ + function bind(f, obj, var_args) { + var a = slice.call(arguments, 2); + return function() { + return f.apply(obj, a.concat(slice.call(arguments))); + }; + } + + var G_vmlCanvasManager_ = { + init: function(opt_doc) { + if (/MSIE/.test(navigator.userAgent) && !window.opera) { + var doc = opt_doc || document; + // Create a dummy element so that IE will allow canvas elements to be + // recognized. + doc.createElement('canvas'); + doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); + } + }, + + init_: function(doc) { + // create xmlns + if (!doc.namespaces['g_vml_']) { + doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml', + '#default#VML'); + + } + if (!doc.namespaces['g_o_']) { + doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office', + '#default#VML'); + } + + // Setup default CSS. Only add one style sheet per document + if (!doc.styleSheets['ex_canvas_']) { + var ss = doc.createStyleSheet(); + ss.owningElement.id = 'ex_canvas_'; + ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + + // default size is 300x150 in Gecko and Opera + 'text-align:left;width:300px;height:150px}' + + 'g_vml_\\:*{behavior:url(#default#VML)}' + + 'g_o_\\:*{behavior:url(#default#VML)}'; + + } + + // find all canvas elements + var els = doc.getElementsByTagName('canvas'); + for (var i = 0; i < els.length; i++) { + this.initElement(els[i]); + } + }, + + /** + * Public initializes a canvas element so that it can be used as canvas + * element from now on. This is called automatically before the page is + * loaded but if you are creating elements using createElement you need to + * make sure this is called on the element. + * @param {HTMLElement} el The canvas element to initialize. + * @return {HTMLElement} the element that was created. + */ + initElement: function(el) { + if (!el.getContext) { + + el.getContext = getContext; + + // Remove fallback content. There is no way to hide text nodes so we + // just remove all childNodes. We could hide all elements and remove + // text nodes but who really cares about the fallback content. + el.innerHTML = ''; + + // do not use inline function because that will leak memory + el.attachEvent('onpropertychange', onPropertyChange); + el.attachEvent('onresize', onResize); + + var attrs = el.attributes; + if (attrs.width && attrs.width.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setWidth_(attrs.width.nodeValue); + el.style.width = attrs.width.nodeValue + 'px'; + } else { + el.width = el.clientWidth; + } + if (attrs.height && attrs.height.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setHeight_(attrs.height.nodeValue); + el.style.height = attrs.height.nodeValue + 'px'; + } else { + el.height = el.clientHeight; + } + //el.getContext().setCoordsize_() + } + return el; + } + }; + + function onPropertyChange(e) { + var el = e.srcElement; + + switch (e.propertyName) { + case 'width': + el.style.width = el.attributes.width.nodeValue + 'px'; + el.getContext().clearRect(); + break; + case 'height': + el.style.height = el.attributes.height.nodeValue + 'px'; + el.getContext().clearRect(); + break; + } + } + + function onResize(e) { + var el = e.srcElement; + if (el.firstChild) { + el.firstChild.style.width = el.clientWidth + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + } + } + + G_vmlCanvasManager_.init(); + + // precompute "00" to "FF" + var dec2hex = []; + for (var i = 0; i < 16; i++) { + for (var j = 0; j < 16; j++) { + dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); + } + } + + function createMatrixIdentity() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ]; + } + + function matrixMultiply(m1, m2) { + var result = createMatrixIdentity(); + + for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var sum = 0; + + for (var z = 0; z < 3; z++) { + sum += m1[x][z] * m2[z][y]; + } + + result[x][y] = sum; + } + } + return result; + } + + function copyState(o1, o2) { + o2.fillStyle = o1.fillStyle; + o2.lineCap = o1.lineCap; + o2.lineJoin = o1.lineJoin; + o2.lineWidth = o1.lineWidth; + o2.miterLimit = o1.miterLimit; + o2.shadowBlur = o1.shadowBlur; + o2.shadowColor = o1.shadowColor; + o2.shadowOffsetX = o1.shadowOffsetX; + o2.shadowOffsetY = o1.shadowOffsetY; + o2.strokeStyle = o1.strokeStyle; + o2.globalAlpha = o1.globalAlpha; + o2.arcScaleX_ = o1.arcScaleX_; + o2.arcScaleY_ = o1.arcScaleY_; + o2.lineScale_ = o1.lineScale_; + } + + function processStyle(styleString) { + var str, alpha = 1; + + styleString = String(styleString); + if (styleString.substring(0, 3) == 'rgb') { + var start = styleString.indexOf('(', 3); + var end = styleString.indexOf(')', start + 1); + var guts = styleString.substring(start + 1, end).split(','); + + str = '#'; + for (var i = 0; i < 3; i++) { + str += dec2hex[Number(guts[i])]; + } + + if (guts.length == 4 && styleString.substr(3, 1) == 'a') { + alpha = guts[3]; + } + } else { + str = styleString; + } + + return {color: str, alpha: alpha}; + } + + function processLineCap(lineCap) { + switch (lineCap) { + case 'butt': + return 'flat'; + case 'round': + return 'round'; + case 'square': + default: + return 'square'; + } + } + + /** + * This class implements CanvasRenderingContext2D interface as described by + * the WHATWG. + * @param {HTMLElement} surfaceElement The element that the 2D context should + * be associated with + */ + function CanvasRenderingContext2D_(surfaceElement) { + this.m_ = createMatrixIdentity(); + + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + + // Canvas context properties + this.strokeStyle = '#000'; + this.fillStyle = '#000'; + + this.lineWidth = 1; + this.lineJoin = 'miter'; + this.lineCap = 'butt'; + this.miterLimit = Z * 1; + this.globalAlpha = 1; + this.canvas = surfaceElement; + + var el = surfaceElement.ownerDocument.createElement('div'); + el.style.width = surfaceElement.clientWidth + 'px'; + el.style.height = surfaceElement.clientHeight + 'px'; + el.style.overflow = 'hidden'; + el.style.position = 'absolute'; + surfaceElement.appendChild(el); + + this.element_ = el; + this.arcScaleX_ = 1; + this.arcScaleY_ = 1; + this.lineScale_ = 1; + } + + var contextPrototype = CanvasRenderingContext2D_.prototype; + contextPrototype.clearRect = function() { + this.element_.innerHTML = ''; + }; + + contextPrototype.beginPath = function() { + // TODO: Branch current matrix so that save/restore has no effect + // as per safari docs. + this.currentPath_ = []; + }; + + contextPrototype.moveTo = function(aX, aY) { + var p = this.getCoords_(aX, aY); + this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.lineTo = function(aX, aY) { + var p = this.getCoords_(aX, aY); + this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); + + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, + aCP2x, aCP2y, + aX, aY) { + var p = this.getCoords_(aX, aY); + var cp1 = this.getCoords_(aCP1x, aCP1y); + var cp2 = this.getCoords_(aCP2x, aCP2y); + bezierCurveTo(this, cp1, cp2, p); + }; + + // Helper function that takes the already fixed cordinates. + function bezierCurveTo(self, cp1, cp2, p) { + self.currentPath_.push({ + type: 'bezierCurveTo', + cp1x: cp1.x, + cp1y: cp1.y, + cp2x: cp2.x, + cp2y: cp2.y, + x: p.x, + y: p.y + }); + self.currentX_ = p.x; + self.currentY_ = p.y; + } + + contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { + // the following is lifted almost directly from + // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes + + var cp = this.getCoords_(aCPx, aCPy); + var p = this.getCoords_(aX, aY); + + var cp1 = { + x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), + y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) + }; + var cp2 = { + x: cp1.x + (p.x - this.currentX_) / 3.0, + y: cp1.y + (p.y - this.currentY_) / 3.0 + }; + + bezierCurveTo(this, cp1, cp2, p); + }; + + contextPrototype.arc = function(aX, aY, aRadius, + aStartAngle, aEndAngle, aClockwise) { + aRadius *= Z; + var arcType = aClockwise ? 'at' : 'wa'; + + var xStart = aX + mc(aStartAngle) * aRadius - Z2; + var yStart = aY + ms(aStartAngle) * aRadius - Z2; + + var xEnd = aX + mc(aEndAngle) * aRadius - Z2; + var yEnd = aY + ms(aEndAngle) * aRadius - Z2; + + // IE won't render arches drawn counter clockwise if xStart == xEnd. + if (xStart == xEnd && !aClockwise) { + xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something + // that can be represented in binary + } + + var p = this.getCoords_(aX, aY); + var pStart = this.getCoords_(xStart, yStart); + var pEnd = this.getCoords_(xEnd, yEnd); + + this.currentPath_.push({type: arcType, + x: p.x, + y: p.y, + radius: aRadius, + xStart: pStart.x, + yStart: pStart.y, + xEnd: pEnd.x, + yEnd: pEnd.y}); + + }; + + contextPrototype.rect = function(aX, aY, aWidth, aHeight) { + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + }; + + contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.stroke(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.fill(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { + var gradient = new CanvasGradient_('gradient'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + return gradient; + }; + + contextPrototype.createRadialGradient = function(aX0, aY0, aR0, + aX1, aY1, aR1) { + var gradient = new CanvasGradient_('gradientradial'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.r0_ = aR0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + gradient.r1_ = aR1; + return gradient; + }; + + contextPrototype.drawImage = function(image, var_args) { + var dx, dy, dw, dh, sx, sy, sw, sh; + + // to find the original width we overide the width and height + var oldRuntimeWidth = image.runtimeStyle.width; + var oldRuntimeHeight = image.runtimeStyle.height; + image.runtimeStyle.width = 'auto'; + image.runtimeStyle.height = 'auto'; + + // get the original size + var w = image.width; + var h = image.height; + + // and remove overides + image.runtimeStyle.width = oldRuntimeWidth; + image.runtimeStyle.height = oldRuntimeHeight; + + if (arguments.length == 3) { + dx = arguments[1]; + dy = arguments[2]; + sx = sy = 0; + sw = dw = w; + sh = dh = h; + } else if (arguments.length == 5) { + dx = arguments[1]; + dy = arguments[2]; + dw = arguments[3]; + dh = arguments[4]; + sx = sy = 0; + sw = w; + sh = h; + } else if (arguments.length == 9) { + sx = arguments[1]; + sy = arguments[2]; + sw = arguments[3]; + sh = arguments[4]; + dx = arguments[5]; + dy = arguments[6]; + dw = arguments[7]; + dh = arguments[8]; + } else { + throw Error('Invalid number of arguments'); + } + + var d = this.getCoords_(dx, dy); + + var w2 = sw / 2; + var h2 = sh / 2; + + var vmlStr = []; + + var W = 10; + var H = 10; + + // For some reason that I've now forgotten, using divs didn't work + vmlStr.push(' ' , + '', + ''); + + this.element_.insertAdjacentHTML('BeforeEnd', + vmlStr.join('')); + }; + + contextPrototype.stroke = function(aFill) { + var lineStr = []; + var lineOpen = false; + var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); + var color = a.color; + var opacity = a.alpha * this.globalAlpha; + + var W = 10; + var H = 10; + + lineStr.push(''); + + if (!aFill) { + var lineWidth = this.lineScale_ * this.lineWidth; + + // VML cannot correctly render a line if the width is less than 1px. + // In that case, we dilute the color to make the line look thinner. + if (lineWidth < 1) { + opacity *= lineWidth; + } + + lineStr.push( + '' + ); + } else if (typeof this.fillStyle == 'object') { + var fillStyle = this.fillStyle; + var angle = 0; + var focus = {x: 0, y: 0}; + + // additional offset + var shift = 0; + // scale factor for offset + var expansion = 1; + + if (fillStyle.type_ == 'gradient') { + var x0 = fillStyle.x0_ / this.arcScaleX_; + var y0 = fillStyle.y0_ / this.arcScaleY_; + var x1 = fillStyle.x1_ / this.arcScaleX_; + var y1 = fillStyle.y1_ / this.arcScaleY_; + var p0 = this.getCoords_(x0, y0); + var p1 = this.getCoords_(x1, y1); + var dx = p1.x - p0.x; + var dy = p1.y - p0.y; + angle = Math.atan2(dx, dy) * 180 / Math.PI; + + // The angle should be a non-negative number. + if (angle < 0) { + angle += 360; + } + + // Very small angles produce an unexpected result because they are + // converted to a scientific notation string. + if (angle < 1e-6) { + angle = 0; + } + } else { + var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_); + var width = max.x - min.x; + var height = max.y - min.y; + focus = { + x: (p0.x - min.x) / width, + y: (p0.y - min.y) / height + }; + + width /= this.arcScaleX_ * Z; + height /= this.arcScaleY_ * Z; + var dimension = m.max(width, height); + shift = 2 * fillStyle.r0_ / dimension; + expansion = 2 * fillStyle.r1_ / dimension - shift; + } + + // We need to sort the color stops in ascending order by offset, + // otherwise IE won't interpret it correctly. + var stops = fillStyle.colors_; + stops.sort(function(cs1, cs2) { + return cs1.offset - cs2.offset; + }); + + var length = stops.length; + var color1 = stops[0].color; + var color2 = stops[length - 1].color; + var opacity1 = stops[0].alpha * this.globalAlpha; + var opacity2 = stops[length - 1].alpha * this.globalAlpha; + + var colors = []; + for (var i = 0; i < length; i++) { + var stop = stops[i]; + colors.push(stop.offset * expansion + shift + ' ' + stop.color); + } + + // When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + lineStr.push(''); + } else { + lineStr.push(''); + } + + lineStr.push(''); + + this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); + }; + + contextPrototype.fill = function() { + this.stroke(true); + } + + contextPrototype.closePath = function() { + this.currentPath_.push({type: 'close'}); + }; + + /** + * @private + */ + contextPrototype.getCoords_ = function(aX, aY) { + var m = this.m_; + return { + x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, + y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 + } + }; + + contextPrototype.save = function() { + var o = {}; + copyState(this, o); + this.aStack_.push(o); + this.mStack_.push(this.m_); + this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); + }; + + contextPrototype.restore = function() { + copyState(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop(); + }; + + function matrixIsFinite(m) { + for (var j = 0; j < 3; j++) { + for (var k = 0; k < 2; k++) { + if (!isFinite(m[j][k]) || isNaN(m[j][k])) { + return false; + } + } + } + return true; + } + + function setM(ctx, m, updateLineScale) { + if (!matrixIsFinite(m)) { + return; + } + ctx.m_ = m; + + if (updateLineScale) { + // Get the line scale. + // Determinant of this.m_ means how much the area is enlarged by the + // transformation. So its square root can be used as a scale factor + // for width. + var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; + ctx.lineScale_ = sqrt(abs(det)); + } + } + + contextPrototype.translate = function(aX, aY) { + var m1 = [ + [1, 0, 0], + [0, 1, 0], + [aX, aY, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.rotate = function(aRot) { + var c = mc(aRot); + var s = ms(aRot); + + var m1 = [ + [c, s, 0], + [-s, c, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.scale = function(aX, aY) { + this.arcScaleX_ *= aX; + this.arcScaleY_ *= aY; + var m1 = [ + [aX, 0, 0], + [0, aY, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { + var m1 = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { + var m = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, m, true); + }; + + /******** STUBS ********/ + contextPrototype.clip = function() { + // TODO: Implement + }; + + contextPrototype.arcTo = function() { + // TODO: Implement + }; + + contextPrototype.createPattern = function() { + return new CanvasPattern_; + }; + + // Gradient / Pattern Stubs + function CanvasGradient_(aType) { + this.type_ = aType; + this.x0_ = 0; + this.y0_ = 0; + this.r0_ = 0; + this.x1_ = 0; + this.y1_ = 0; + this.r1_ = 0; + this.colors_ = []; + } + + CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { + aColor = processStyle(aColor); + this.colors_.push({offset: aOffset, + color: aColor.color, + alpha: aColor.alpha}); + }; + + function CanvasPattern_() {} + + // set up externs + G_vmlCanvasManager = G_vmlCanvasManager_; + CanvasRenderingContext2D = CanvasRenderingContext2D_; + CanvasGradient = CanvasGradient_; + CanvasPattern = CanvasPattern_; + +})(); + +} // if diff --git a/lib/excanvas.min.js b/lib/excanvas.min.js new file mode 100644 index 0000000..a34ca1d --- /dev/null +++ b/lib/excanvas.min.js @@ -0,0 +1,35 @@ +// Copyright 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_|| +b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d','","");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("g.x)g.x=e.x;if(h.y==null||e.yg.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_), +z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l')}else a.push('');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("')}a.push("");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(), +this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a, +0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager= +M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})(); diff --git a/lib/functional.js b/lib/functional.js new file mode 100644 index 0000000..c1742f3 --- /dev/null +++ b/lib/functional.js @@ -0,0 +1,1050 @@ +/* + * Author: Oliver Steele + * Copyright: Copyright 2007 by Oliver Steele. All rights reserved. + * License: MIT License + * Homepage: http://osteele.com/javascripts/functional + * Source: http://osteele.com/javascripts/functional/functional.js + * Changes: http://osteele.com/javascripts/functional/CHANGES + * Created: 2007-07-11 + * Version: 1.0.2 + * + * + * This file defines some higher-order methods and functions for + * functional and function-level programming. + */ + +/// `Functional` is the namespace for higher-order functions. +var Functional = this.Functional || {}; + +/** + * This function copies all the public functions in `Functional` except itself + * into the global namespace. If the optional argument $except$ is present, + * functions named by its property names are not copied. + * >> Functional.install() + */ +Functional.install = function(except) { + var source = Functional, + target = (function() { return this; })(); // References the global object. + for (var name in source) + name == 'install' + || name.charAt(0) == '_' + || except && name in except + || !source.hasOwnProperty(name) // work around Prototype + || (target[name] = source[name]); +} + +/// ^ Higher-order functions + +/** + * Returns a function that applies the last argument of this + * function to its input, and the penultimate argument to the + * result of the application, and so on. + * == compose(f1, f2, f3..., fn)(args) == f1(f2(f3(...(fn(args...))))) + * :: (a2 -> a1) (a3 -> a2)... (a... -> a_{n}) -> a... -> a1 + * >> compose('1+', '2*')(2) -> 5 + */ +Functional.compose = function(/*fn...*/) { + var fns = Functional.map(Function.toFunction, arguments), + arglen = fns.length; + return function() { + for (var i = arglen; --i >= 0; ) + arguments = [fns[i].apply(this, arguments)]; + return arguments[0]; + } +} + +/** + * Same as `compose`, except applies the functions in argument-list order. + * == sequence(f1, f2, f3..., fn)(args...) == fn(...(f3(f2(f1(args...))))) + * :: (a... -> a1) (a1 -> a2) (a2 -> a3)... (a_{n-1} -> a_{n}) -> a... -> a_{n} + * >> sequence('1+', '2*')(2) -> 6 + */ +Functional.sequence = function(/*fn...*/) { + var fns = Functional.map(Function.toFunction, arguments), + arglen = fns.length; + return function() { + for (var i = 0; i < arglen; i++) + arguments = [fns[i].apply(this, arguments)]; + return arguments[0]; + } +} + +/** + * Applies `fn` to each element of `sequence`. + * == map(f, [x1, x2...]) = [f(x, 0), f(x2, 1), ...] + * :: (a ix -> boolean) [a] -> [a] + * >> map('1+', [1,2,3]) -> [2, 3, 4] + * + * If `object` is supplied, it is the object of the call. + * + * The fusion rule: + * >> map('+1', map('*2', [1,2,3])) -> [3, 5, 7] + * >> map(compose('+1', '*2'), [1,2,3]) -> [3, 5, 7] + */ +Functional.map = function(fn, sequence, object) { + fn = Function.toFunction(fn); + var len = sequence.length, + result = new Array(len); + for (var i = 0; i < len; i++) + result[i] = fn.apply(object, [sequence[i], i]); + return result; +} + +/** + * Applies `fn` to `init` and the first element of `sequence`, + * and then to the result and the second element, and so on. + * == reduce(f, init, [x0, x1, x2]) == f(f(f(init, x0), x1), x2) + * :: (a b -> a) a [b] -> a + * >> reduce('x y -> 2*x+y', 0, [1,0,1,0]) -> 10 + */ +Functional.reduce = function(fn, init, sequence, object) { + fn = Function.toFunction(fn); + var len = sequence.length, + result = init; + for (var i = 0; i < len; i++) + result = fn.apply(object, [result, sequence[i]]); + return result; +} + +/** + * Returns a list of those elements $x$ of `sequence` such that + * $fn(x)$ returns true. + * :: (a -> boolean) [a] -> [a] + * >> select('%2', [1,2,3,4]) -> [1, 3] + */ +Functional.select = function(fn, sequence, object) { + fn = Function.toFunction(fn); + var len = sequence.length, + result = []; + for (var i = 0; i < len; i++) { + var x = sequence[i]; + fn.apply(object, [x, i]) && result.push(x); + } + return result; +} + +/// A synonym for `select`. +Functional.filter = Functional.select; + +/// A synonym for `reduce`. +Functional.foldl = Functional.reduce; + +/** + * Same as `foldl`, but applies the function from right to left. + * == foldr(f, init, [x0, x1, x2]) == fn(x0, f(x1, f(x2, init))) + * :: (a b -> b) b [a] -> b + * >> foldr('x y -> 2*x+y', 100, [1,0,1,0]) -> 104 + */ +Functional.foldr = function(fn, init, sequence, object) { + fn = Function.toFunction(fn); + var len = sequence.length, + result = init; + for (var i = len; --i >= 0; ) + result = fn.apply(object, [sequence[i], result]); + return result; +} + +/// ^^ Predicates + +/** + * Returns a function that returns `true` when all the arguments, applied + * to the returned function's arguments, returns true. + * == and(f1, f2...)(args...) == f1(args...) && f2(args...)... + * :: [a -> boolean] a -> a + * >> and('>1', '>2')(2) -> false + * >> and('>1', '>2')(3) -> true + * >> and('>1', 'error()')(1) -> false + */ +Functional.and = function(/*functions...*/) { + var args = Functional.map(Function.toFunction, arguments), + arglen = args.length; + return function() { + var value = true; + for (var i = 0; i < arglen; i++) + if (!(value = args[i].apply(this, arguments))) + break; + return value; + } +} + +/** + * Returns a function that returns `true` when any argument, applied + * to the returned function's arguments, returns true. + * == or(f1, f2...)(args...) == f1(args...) || f2(args...)... + * :: [a -> boolean] a -> a + * >> or('>1', '>2')(1) -> false + * >> or('>1', '>2')(2) -> true + * >> or('>1', 'error()')(2) -> true + */ +Functional.or = function(/*functions...*/) { + var args = Functional.map(Function.toFunction, arguments), + arglen = args.length; + return function() { + var value = false; + for (var i = 0; i < arglen; i++) + if ((value = args[i].apply(this, arguments))) + break; + return value; + } +} + +/** + * Returns true when $fn(x)$ returns true for some element $x$ of + * `sequence`. The returned function short-circuits. + * == some(f, [x1, x2, x3, ...]) == f(x1) || f(x2) || f(x3)... + * :: (a -> boolean) [a] -> boolean + * >> some('>2', [1,2,3]) -> true + * >> some('>10', [1,2,3]) -> false + */ +Functional.some = function(fn, sequence, object) { + fn = Function.toFunction(fn); + var len = sequence.length, + value = false; + for (var i = 0; i < len; i++) + if ((value = fn.call(object, sequence[i]))) + break; + return value; +} + +/** + * Returns true when $fn(x)$ returns true for every element $x$ of + * `sequence`. The returned function short-circuits. + * == every(f, [x1, x2, x3, ...]) == f(x1) && f(x2) && f(x3)... + * :: (a -> boolean) [a] -> boolean + * >> every('<2', [1,2,3]) -> false + * >> every('<10', [1,2,3]) -> true + */ +Functional.every = function(fn, sequence, object) { + fn = Function.toFunction(fn); + var len = sequence.length, + value = true; + for (var i = 0; i < len; i++) + if (!(value = fn.call(object, sequence[i]))) + break; + return value; +} + +/** + * Returns a function that returns `true` when $fn()$ returns false. + * == f.not()(args...) == !f(args...) + * :: (a -> boolean) -> (a -> boolean) + * >> not(Functional.K(true))() -> false + * >> not(Functional.K(false))() -> true + */ +Functional.not = function(fn) { + fn = Function.toFunction(fn); + return function() { + return !fn.apply(null, arguments); + } +} + +/** + * Returns a function that returns true when this function's arguments + * applied to that functions are always the same. The returned function + * short-circuits. + * == equal(f1, f2...)(args...) == f1(args...) == f2(args...)... + * :: [a... -> b] -> a... -> b + * >> equal()() -> true + * >> equal(K(1))() -> true + * >> equal(K(1), K(1))() -> true + * >> equal(K(1), K(2))() -> false + * >> equal(K(1), K(2), 'error()')() -> false + */ +Functional.equal = function(/*fn...*/) { + var arglen = arguments.length, + args = Functional.map(Function.toFunction, arguments); + if (!arglen) return Functional.K(true); + // if arglen == 1 it's also constant true, but + // call it for effect. + return function() { + var value = args[0].apply(this, arguments); + for (var i = 1; i < arglen; i++) + if (value != args[i].apply(this, args)) + return false; + return true; + } +} + + +/// ^^ Utilities + +/** + * Returns its argument coerced to a function. + * >> lambda('1+')(2) -> 3 + * >> lambda(function(n){return n+1})(2) -> 3 + */ +Functional.lambda = function(object) { + return object.toFunction(); +} + + +/** + * Returns a function that takes an object as an argument, and applies + * `object`'s `methodName` method to `arguments`. + * == invoke(name)(object, args...) == object[name](args...) + * :: name args... -> object args2... -> object[name](args... args2...) + * >> invoke('toString')(123) -> "123" + */ +Functional.invoke = function(methodName/*, arguments*/) { + var args = Array.slice(arguments, 1); + return function(object) { + return object[methodName].apply(object, Array.slice(arguments, 1).concat(args)); + } +} + +/** + * Returns a function that takes an object, and returns the value of its + * `name` property. `pluck(name)` is equivalent to `'_.name'.lambda()`. + * == pluck(name)(object) == object[name] + * :: name -> object -> object[name] + * >> pluck('length')("abc") -> 3 + */ +Functional.pluck = function(name) { + return function(object) { + return object[name]; + } +} + +/** + * Returns a function that, while $pred(value)$ is true, applies `fn` to + * $value$ to produce a new value, which is used as an input for the next round. + * The returned function returns the first $value$ for which $pred(value)$ + * is false. + * :: (a -> boolean) (a -> a) -> a + * >> until('>10', '2*')(1) -> 16 + */ +Functional.until = function(pred, fn) { + fn = Function.toFunction(fn); + pred = Function.toFunction(pred); + return function(value) { + while (!pred.call(null, value)) + value = fn.call(null, value); + return value; + } +} + +/** + * :: [a] [b]... -> [[a b]...] + * == zip(a, b...) == [[a0, b0], [a1, b1], ...] + * Did you know that `zip` can transpose a matrix? + * >> zip.apply(null, [[1,2],[3,4]]) -> [[1, 3], [2, 4]] + */ +Functional.zip = function(/*args...*/) { + var n = Math.min.apply(null, Functional.map('.length', arguments)); + var results = new Array(n); + for (var i = 0; i < n; i++) { + var key = String(i); + results[key] = Functional.map(pluck(key), arguments); + }; + return results; +} + +Functional._startRecordingMethodChanges = function(object) { + var initialMethods = {}; + for (var name in object) + initialMethods[name] = object[name]; + return {getChangedMethods: function() { + var changedMethods = {}; + for (var name in object) + if (object[name] != initialMethods[name]) + changedMethods[name] = object[name]; + return changedMethods; + }}; +} + +// For each method that this file defined on `Function.prototype`, +// define a function on `Functional` that delegates to it. +Functional._attachMethodDelegates = function(methods) { + for (var name in methods) + Functional[name] = Functional[name] || (function(name) { + var fn = methods[name]; + return function(object) { + return fn.apply(Function.toFunction(object), Array.slice(arguments, 1)); + } + })(name); +} + +// Record the current contents of `Function.prototype`, so that we +// can see what we've added later. +Functional.__initalFunctionState = Functional._startRecordingMethodChanges(Function.prototype); + +/// ^ Higher-order methods + +/// ^^ Partial function application + +/** + * Returns a bound method on `object`, optionally currying `args`. + * == f.bind(obj, args...)(args2...) == f.apply(obj, [args..., args2...]) + */ +Function.prototype.bind = function(object/*, args...*/) { + var fn = this; + var args = Array.slice(arguments, 1); + return function() { + return fn.apply(object, args.concat(Array.slice(arguments, 0))); + } +} + +/** + * Returns a function that applies the underlying function to `args`, and + * ignores its own arguments. + * :: (a... -> b) a... -> (... -> b) + * == f.saturate(args...)(args2...) == f(args...) + * >> Math.max.curry(1, 2)(3, 4) -> 4 + * >> Math.max.saturate(1, 2)(3, 4) -> 2 + * >> Math.max.curry(1, 2).saturate()(3, 4) -> 2 + */ +Function.prototype.saturate = function(/*args*/) { + var fn = this; + var args = Array.slice(arguments, 0); + return function() { + return fn.apply(this, args); + } +} + +/** + * Invoking the function returned by this function only passes `n` + * arguments to the underlying function. If the underlying function + * is not saturated, the result is a function that passes all its + * arguments to the underlying function. (That is, `aritize` only + * affects its immediate caller, and not subsequent calls.) + * >> '[a,b]'.lambda()(1,2) -> [1, 2] + * >> '[a,b]'.lambda().aritize(1)(1,2) -> [1, undefined] + * >> '+'.lambda()(1,2)(3) -> error + * >> '+'.lambda().ncurry(2).aritize(1)(1,2)(3) -> 4 + * + * `aritize` is useful to remove optional arguments from a function that + * is passed to a higher-order function that supplies *different* optional + * arguments. + * + * For example, many implementations of `map` and other collection + * functions, including those in this library, call the function argument + * with both the collection element + * and its position. This is convenient when expected, but can wreak + * havoc when the function argument is a curried function that expects + * a single argument from `map` and the remaining arguments from when + * the result of `map` is applied. + */ +Function.prototype.aritize = function(n) { + var fn = this; + return function() { + return fn.apply(this, Array.slice(arguments, 0, n)); + } +} + +/** + * Returns a function that, applied to an argument list $arg2$, + * applies the underlying function to $args ++ arg2$. + * :: (a... b... -> c) a... -> (b... -> c) + * == f.curry(args1...)(args2...) == f(args1..., args2...) + * + * Note that, unlike in languages with true partial application such as Haskell, + * `curry` and `uncurry` are not inverses. This is a repercussion of the + * fact that in JavaScript, unlike Haskell, a fully saturated function is + * not equivalent to the value that it returns. The definition of `curry` + * here matches semantics that most people have used when implementing curry + * for procedural languages. + * + * This implementation is adapted from + * [http://www.coryhudson.com/blog/2007/03/10/javascript-currying-redux/]. + */ +Function.prototype.curry = function(/*args...*/) { + var fn = this; + var args = Array.slice(arguments, 0); + return function() { + return fn.apply(this, args.concat(Array.slice(arguments, 0))); + }; +} + +/* + * Right curry. Returns a function that, applied to an argument list $args2$, + * applies the underlying function to $args2 + args$. + * == f.curry(args1...)(args2...) == f(args2..., args1...) + * :: (a... b... -> c) b... -> (a... -> c) + */ +Function.prototype.rcurry = function(/*args...*/) { + var fn = this; + var args = Array.slice(arguments, 0); + return function() { + return fn.apply(this, Array.slice(arguments, 0).concat(args)); + }; +} + +/** + * Same as `curry`, except only applies the function when all + * `n` arguments are saturated. + */ +Function.prototype.ncurry = function(n/*, args...*/) { + var fn = this; + var largs = Array.slice(arguments, 1); + return function() { + var args = largs.concat(Array.slice(arguments, 0)); + if (args.length < n) { + args.unshift(n); + return fn.ncurry.apply(fn, args); + } + return fn.apply(this, args); + }; +} + +/** + * Same as `rcurry`, except only applies the function when all + * `n` arguments are saturated. + */ +Function.prototype.rncurry = function(n/*, args...*/) { + var fn = this; + var rargs = Array.slice(arguments, 1); + return function() { + var args = Array.slice(arguments, 0).concat(rargs); + if (args.length < n) { + args.unshift(n); + return fn.rncurry.apply(fn, args); + } + return fn.apply(this, args); + }; +} + +/** + * `_` (underscore) is bound to a unique value for use in `partial`, below. + * This is a global variable, but it's also a property of `Function` in case + * you overwrite or bind over the global one. + */ +_ = Function._ = {}; + +/** + * Returns a function $f$ such that $f(args2)$ is equivalent to + * the underlying function applied to a combination of $args$ and $args2$. + * + * `args` is a partially-specified argument: it's a list with "holes", + * specified by the special value `_`. It is combined with $args2$ as + * follows: + * + * From left to right, each value in $args2$ fills in the leftmost + * remaining hole in `args`. Any remaining values + * in $args2$ are appended to the result of the filling-in process + * to produce the combined argument list. + * + * If the combined argument list contains any occurrences of `_`, the result + * of the application of $f$ is another partial function. Otherwise, the + * result is the same as the result of applying the underlying function to + * the combined argument list. + */ +Function.prototype.partial = function(/*args*/) { + var fn = this; + var _ = Function._; + var args = Array.slice(arguments, 0); + //substitution positions + var subpos = [], value; + for (var i = 0; i < arguments.length; i++) + arguments[i] == _ && subpos.push(i); + return function() { + var specialized = args.concat(Array.slice(arguments, subpos.length)); + for (var i = 0; i < Math.min(subpos.length, arguments.length); i++) + specialized[subpos[i]] = arguments[i]; + for (var i = 0; i < specialized.length; i++) + if (specialized[i] == _) + return fn.partial.apply(fn, specialized); + return fn.apply(this, specialized); + } +} + +/// ^^ Combinators + +/// ^^^ Combinator Functions + +/** + * The identity function: $x -> x$. + * == I(x) == x + * == I == 'x'.lambda() + * :: a -> a + * >> Functional.I(1) -> 1 + */ +Functional.I = function(x) {return x}; + +/** + * Returns a constant function that returns `x`. + * == K(x)(y) == x + * :: a -> b -> a + * >> Functional.K(1)(2) -> 1 + */ +Functional.K = function(x) {return function() {return x}}; + +/// A synonym for `Functional.I` +Functional.id = Functional.I; + +/// A synonym for `Functional.K` +Functional.constfn = Functional.K; + + +/** + * Returns a function that applies the first function to the + * result of the second, but passes all its arguments too. + * == S(f, g)(args...) == f(g(args...), args...) + * + * This is useful for composing functions when each needs access + * to the arguments to the composed function. For example, + * the following function multiples its last two arguments, + * and adds the first to that. + * >> Function.S('+', '_ a b -> a*b')(2,3,4) -> 14 + * + * Curry this to get a version that takes its arguments in + * separate calls: + * >> Function.S.curry('+')('_ a b -> a*b')(2,3,4) -> 14 + */ +Function.S = function(f, g) { + f = Function.toFunction(f); + g = Function.toFunction(g); + return function() { + return f.apply(this, [g.apply(this, arguments)].concat(Array.slice(arguments, 0))); + } +} + +/// ^^^ Combinator methods + +/** + * Returns a function that swaps its first two arguments before + * passing them to the underlying function. + * == f.flip()(a, b, c...) == f(b, a, c...) + * :: (a b c...) -> (b a c...) + * >> ('a/b'.lambda()).flip()(1,2) -> 2 + * + * For more general derangements, you can also use `prefilterSlice` + * with a string lambda: + * >> '100*a+10*b+c'.lambda().prefilterSlice('a b c -> [b, c, a]')(1,2,3) -> 231 + */ +Function.prototype.flip = function() { + var fn = this; + return function() { + var args = Array.slice(arguments, 0); + args = args.slice(1,2).concat(args.slice(0,1)).concat(args.slice(2)); + return fn.apply(this, args); + } +} + +/** + * Returns a function that applies the underlying function to its + * first argument, and the result of that application to the remaining + * arguments. + * == f.uncurry(a, b...) == f(a)(b...) + * :: (a -> b -> c) -> (a, b) -> c + * >> 'a -> b -> a/b'.lambda().uncurry()(1,2) -> 0.5 + * + * Note that `uncurry` is *not* the inverse of `curry`. + */ +Function.prototype.uncurry = function() { + var fn = this; + return function() { + var f1 = fn.apply(this, Array.slice(arguments, 0, 1)); + return f1.apply(this, Array.slice(arguments, 1)); + } +} + +/** + * ^^ Filtering + * + * Filters intercept a value before it is passed to a function, and apply the + * underlying function to the modified value. + */ + +/** + * `prefilterObject` returns a function that applies the underlying function + * to the same arguments, but to an object that is the result of appyling + * `filter` to the invocation object. + * == fn.prefilterObject(filter).apply(object, args...) == fn.apply(filter(object), args...) + * == fn.bind(object) == compose(fn.prefilterObject, Functional.K(object)) + * >> 'this'.lambda().prefilterObject('n+1').apply(1) -> 2 + */ +Function.prototype.prefilterObject = function(filter) { + filter = Function.toFunction(filter); + var fn = this; + return function() { + return fn.apply(filter(this), arguments); + } +} + +/** + * `prefilterAt` returns a function that applies the underlying function + * to a copy of the arguments, where the `index`th argument has been + * replaced by the value of `filter(argument[index])`. + * == fn.prefilterAt(i, filter)(a1, a2, ..., a_{n}) == fn(a1, a2, ..., filter(a_{i}), ..., a_{n}) + * >> '[a,b,c]'.lambda().prefilterAt(1, '2*')(2,3,4) -> [2, 6, 4] + */ +Function.prototype.prefilterAt = function(index, filter) { + filter = Function.toFunction(filter); + var fn = this; + return function() { + var args = Array.slice(arguments, 0); + args[index] = filter.call(this, args[index]); + return fn.apply(this, args); + } +} + +/** + * `prefilterSlice` returns a function that applies the underlying function + * to a copy of the arguments, where the arguments `start` through + * `end` have been replaced by the value of `filter(argument.slice(start,end))`, + * which must return a list. + * == fn.prefilterSlice(i0, i1, filter)(a1, a2, ..., a_{n}) == fn(a1, a2, ..., filter(args_{i0}, ..., args_{i1}), ..., a_{n}) + * >> '[a,b,c]'.lambda().prefilterSlice('[a+b]', 1, 3)(1,2,3,4) -> [1, 5, 4] + * >> '[a,b]'.lambda().prefilterSlice('[a+b]', 1)(1,2,3) -> [1, 5] + * >> '[a]'.lambda().prefilterSlice(compose('[_]', Math.max))(1,2,3) -> [3] + */ +Function.prototype.prefilterSlice = function(filter, start, end) { + filter = Function.toFunction(filter); + start = start || 0; + var fn = this; + return function() { + var args = Array.slice(arguments, 0); + var e = end < 0 ? args.length + end : end || args.length; + args.splice.apply(args, [start, (e||args.length)-start].concat(filter.apply(this, args.slice(start, e)))); + return fn.apply(this, args); + } +} + +/// ^^ Method Composition + +/** + * `compose` returns a function that applies the underlying function + * to the result of the application of `fn`. + * == f.compose(g)(args...) == f(g(args...)) + * >> '1+'.lambda().compose('2*')(3) -> 7 + * + * Note that, unlike `Functional.compose`, the `compose` method on + * function only takes a single argument. + * == Functional.compose(f, g) == f.compose(g) + * == Functional.compose(f, g, h) == f.compose(g).compose(h) + */ +Function.prototype.compose = function(fn) { + var self = this; + fn = Function.toFunction(fn); + return function() { + return self.apply(this, [fn.apply(this, arguments)]); + } +} + +/** + * `sequence` returns a function that applies the underlying function + * to the result of the application of `fn`. + * == f.sequence(g)(args...) == g(f(args...)) + * == f.sequence(g) == g.compose(f) + * >> '1+'.lambda().sequence('2*')(3) -> 8 + * + * Note that, unlike `Functional.compose`, the `sequence` method on + * function only takes a single argument. + * == Functional.sequence(f, g) == f.sequence(g) + * == Functional.sequence(f, g, h) == f.sequence(g).sequence(h) + */ +Function.prototype.sequence = function(fn) { + var self = this; + fn = Function.toFunction(fn); + return function() { + return fn.apply(this, [self.apply(this, arguments)]); + } +} + +/** + * Returns a function that is equivalent to the underlying function when + * `guard` returns true, and otherwise is equivalent to the application + * of `otherwise` to the same arguments. + * + * `guard` and `otherwise` default to `Functional.I`. `guard` with + * no arguments therefore returns a function that applies the + * underlying function to its value only if the value is true, + * and returns the value otherwise. + * == f.guard(g, h)(args...) == f(args...), when g(args...) is true + * == f.guard(g ,h)(args...) == h(args...), when g(args...) is false + * >> '[_]'.lambda().guard()(1) -> [1] + * >> '[_]'.lambda().guard()(null) -> null + * >> '[_]'.lambda().guard(null, Functional.K('n/a'))(null) -> "n/a" + * >> 'x+1'.lambda().guard('<10', Functional.K(null))(1) -> 2 + * >> 'x+1'.lambda().guard('<10', Functional.K(null))(10) -> null + * >> '/'.lambda().guard('p q -> q', Functional.K('n/a'))(1, 2) -> 0.5 + * >> '/'.lambda().guard('p q -> q', Functional.K('n/a'))(1, 0) -> "n/a" + * >> '/'.lambda().guard('p q -> q', '-> "n/a"')(1, 0) -> "n/a" + */ +Function.prototype.guard = function(guard, otherwise) { + var fn = this; + guard = Function.toFunction(guard || Functional.I); + otherwise = Function.toFunction(otherwise || Functional.I); + return function() { + return (guard.apply(this, arguments) ? fn : otherwise).apply(this, arguments); + } +} + +/// ^^ Utilities + +/** + * Returns a function identical to this function except that + * it prints its arguments on entry and its return value on exit. + * This is useful for debugging function-level programs. + */ +Function.prototype.traced = function(name) { + var self = this, + global = (function() { return this; })(), + log = function() {}; + + if (typeof console != 'undefined' && typeof console.info == 'function') { + log = console.info; + } else if (typeof print == 'function') { + log = print; + } + + name = name || self; + return function() { + log('[', name, 'apply(', this!=global && this, ',', arguments, ')'); + var result = self.apply(this, arguments); + log(']', name, ' -> ', result); + return result; + } +} + + +/** + * ^^ Function methods as functions + * + * In addition to the functions defined above, every method defined + * on `Function` is also available as a function in `Functional`, that + * coerces its first argument to a `Function` and applies + * the remaining arguments to this. + * + * A few examples make this clearer: + * == curry(fn, args...) == fn.curry(args...) + * >> Functional.flip('a/b')(1, 2) -> 2 + * >> Functional.curry('a/b', 1)(2) -> 0.5 + + * For each method that this file defined on Function.prototype, + * define a function on Functional that delegates to it. + */ +Functional._attachMethodDelegates(Functional.__initalFunctionState.getChangedMethods()); +delete Functional.__initalFunctionState; + + +// In case to-function.js isn't loaded. +Function.toFunction = Function.toFunction || Functional.K; + +if (!Array.slice) { // mozilla already supports this + Array.slice = (function(slice) { + return function(object) { + return slice.apply(object, slice.call(arguments, 1)); + }; + })(Array.prototype.slice); +} +/* + * Author: Oliver Steele + * Copyright: Copyright 2007 by Oliver Steele. All rights reserved. + * License: MIT License + * Homepage: http://osteele.com/javascripts/functional + * Created: 2007-07-11 + * Version: 1.0.2 + * + * + * This defines "string lambdas", that allow strings such as `x+1` and + * `x -> x+1` to be used in some contexts as functions. + */ + + +/// ^ String lambdas + +/** + * Turns a string that contains a JavaScript expression into a + * `Function` that returns the value of that expression. + * + * If the string contains a `->`, this separates the parameters from the body: + * >> 'x -> x + 1'.lambda()(1) -> 2 + * >> 'x y -> x + 2*y'.lambda()(1, 2) -> 5 + * >> 'x, y -> x + 2*y'.lambda()(1, 2) -> 5 + * + * Otherwise, if the string contains a `_`, this is the parameter: + * >> '_ + 1'.lambda()(1) -> 2 + * + * Otherwise if the string begins or ends with an operator or relation, + * prepend or append a parameter. (The documentation refers to this type + * of string as a "section".) + * >> '/2'.lambda()(4) -> 2 + * >> '2/'.lambda()(4) -> 0.5 + * >> '/'.lambda()(2,4) -> 0.5 + * Sections can end, but not begin with, `-`. (This is to avoid interpreting + * e.g. `-2*x` as a section). On the other hand, a string that either begins + * or ends with `/` is a section, so an expression that begins or ends with a + * regular expression literal needs an explicit parameter. + * + * Otherwise, each variable name is an implicit parameter: + * >> 'x + 1'.lambda()(1) -> 2 + * >> 'x + 2*y'.lambda()(1, 2) -> 5 + * >> 'y + 2*x'.lambda()(1, 2) -> 5 + * + * Implicit parameter detection ignores strings literals, variable names that + * start with capitals, and identifiers that precede `:` or follow `.`: + * >> map('"im"+root', ["probable", "possible"]) -> ["improbable", "impossible"] + * >> 'Math.cos(angle)'.lambda()(Math.PI) -> -1 + * >> 'point.x'.lambda()({x:1, y:2}) -> 1 + * >> '({x:1, y:2})[key]'.lambda()('x') -> 1 + * + * Implicit parameter detection mistakenly looks inside regular expression + * literals for variable names. It also doesn't know to ignore JavaScript + * keywords and bound variables. (The only way you can get these last two is + * with a function literal inside the string. This is outside the intended use + * case for string lambdas.) + * + * Use `_` (to define a unary function) or `->`, if the string contains anything + * that looks like a free variable but shouldn't be used as a parameter, or + * to specify parameters that are ordered differently from their first + * occurrence in the string. + * + * Chain `->`s to create a function in uncurried form: + * >> 'x -> y -> x + 2*y'.lambda()(1)(2) -> 5 + * >> 'x -> y -> z -> x + 2*y+3*z'.lambda()(1)(2)(3) -> 14 + * + * `this` and `arguments` are special: + * >> 'this'.call(1) -> 1 + * >> '[].slice.call(arguments, 0)'.call(null,1,2) -> [1, 2] + */ +String.prototype.lambda = function() { + var params = [], + expr = this, + sections = expr.ECMAsplit(/\s*->\s*/m); + if (sections.length > 1) { + while (sections.length) { + expr = sections.pop(); + params = sections.pop().split(/\s*,\s*|\s+/m); + sections.length && sections.push('(function('+params+'){return ('+expr+')})'); + } + } else if (expr.match(/\b_\b/)) { + params = '_'; + } else { + // test whether an operator appears on the left (or right), respectively + var leftSection = expr.match(/^\s*(?:[+*\/%&|\^\.=<>]|!=)/m), + rightSection = expr.match(/[+\-*\/%&|\^\.=<>!]\s*$/m); + if (leftSection || rightSection) { + if (leftSection) { + params.push('$1'); + expr = '$1' + expr; + } + if (rightSection) { + params.push('$2'); + expr = expr + '$2'; + } + } else { + // `replace` removes symbols that are capitalized, follow '.', + // precede ':', are 'this' or 'arguments'; and also the insides of + // strings (by a crude test). `match` extracts the remaining + // symbols. + var vars = this.replace(/(?:\b[A-Z]|\.[a-zA-Z_$])[a-zA-Z_$\d]*|[a-zA-Z_$][a-zA-Z_$\d]*\s*:|this|arguments|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, '').match(/([a-z_$][a-z_$\d]*)/gi) || []; // ' + for (var i = 0, v; v = vars[i++]; ) + params.indexOf(v) >= 0 || params.push(v); + } + } + return new Function(params, 'return (' + expr + ')'); +} + +/// Turn on caching for `string` -> `Function` conversion. +String.prototype.lambda.cache = function() { + var proto = String.prototype, + cache = {}, + uncached = proto.lambda, + cached = function() { + var key = '#' + this; // avoid hidden properties on Object.prototype + return cache[key] || (cache[key] = uncached.call(this)); + }; + cached.cached = function(){}; + cached.uncache = function(){proto.lambda = uncached}; + proto.lambda = cached; +} + +/** + * ^^ Duck-Typing + * + * Strings support `call` and `apply`. This duck-types them as + * functions, to some callers. + */ + +/** + * Coerce the string to a function and then apply it. + * >> 'x+1'.apply(null, [2]) -> 3 + * >> '/'.apply(null, [2, 4]) -> 0.5 + */ +String.prototype.apply = function(thisArg, args) { + return this.toFunction().apply(thisArg, args); +} + +/** + * Coerce the string to a function and then call it. + * >> 'x+1'.call(null, 2) -> 3 + * >> '/'.call(null, 2, 4) -> 0.5 + */ +String.prototype.call = function() { + return this.toFunction().apply(arguments[0], + Array.prototype.slice.call(arguments, 1)); +} + +/// ^^ Coercion + +/** + * Returns a `Function` that perfoms the action described by this + * string. If the string contains a `return`, applies + * `new Function` to it. Otherwise, this function returns + * the result of `this.lambda()`. + * >> '+1'.toFunction()(2) -> 3 + * >> 'return 1'.toFunction()(1) -> 1 + */ +String.prototype.toFunction = function() { + var body = this; + if (body.match(/\breturn\b/)) + return new Function(this); + return this.lambda(); +} + +/** + * Returns this function. `Function.toFunction` calls this. + * >> '+1'.lambda().toFunction()(2) -> 3 + */ +Function.prototype.toFunction = function() { + return this; +} + +/** + * Coerces `fn` into a function if it is not already one, + * by calling its `toFunction` method. + * >> Function.toFunction(function() {return 1})() -> 1 + * >> Function.toFunction('+1')(2) -> 3 + * + * `Function.toFunction` requires an argument that can be + * coerced to a function. A nullary version can be + * constructed via `guard`: + * >> Function.toFunction.guard()('1+') -> function() + * >> Function.toFunction.guard()(null) -> null + * + * `Function.toFunction` doesn't coerce arbitrary values to functions. + * It might seem convenient to treat + * `Function.toFunction(value)` as though it were the + * constant function that returned `value`, but it's rarely + * useful and it hides errors. Use `Functional.K(value)` instead, + * or a lambda string when the value is a compile-time literal: + * >> Functional.K('a string')() -> "a string" + * >> Function.toFunction('"a string"')() -> "a string" + */ +Function.toFunction = function(value) { + return value.toFunction(); +} + +// Utilities + +// IE6 split is not ECMAScript-compliant. This breaks '->1'.lambda(). +// ECMAsplit is an ECMAScript-compliant `split`, although only for +// one argument. +String.prototype.ECMAsplit = + // The test is from the ECMAScript reference. + ('ab'.split(/a*/).length > 1 + ? String.prototype.split + : function(separator, limit) { + if (typeof limit != 'undefined') + throw "ECMAsplit: limit is unimplemented"; + var result = this.split.apply(this, arguments), + re = RegExp(separator), + savedIndex = re.lastIndex, + match = re.exec(this); + if (match && match.index == 0) + result.unshift(''); + // in case `separator` was already a RegExp: + re.lastIndex = savedIndex; + return result; + }); diff --git a/lib/gury.js b/lib/gury.js new file mode 100644 index 0000000..172dd88 --- /dev/null +++ b/lib/gury.js @@ -0,0 +1,391 @@ +/* + gury.js - A jQuery inspired canvas utility library + + Copyright (c) 2010 Ryan Sandor Richards + + 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. +*/ + +window.$g = window.Gury = (function() { + /* + * Utility functions + */ + function isObject(v) { return typeof v == "object"; } + function isFunction(v) { return typeof v == "function"; } + function isString(v) { return typeof v == "string"; } + function isObjectOrFunction(v) { return typeof v == "function" || typeof v == "object"; } + + function _each(closure) { + for (var i = 0; i < this.length; i++) { + closure(this[i], i); + } + } + + /* + * Internal exception handling + */ + var _failWithException = true; + + function GuryException(msg) { + if (_failWithException) { + throw "Gury: " + msg; + } + } + + /* + * These handle mappings from Canvas DOM elements to Gury instances + * to allow for persistant states between calls to the module. + */ + var guryId = 1; + var canvasToGury = {}; + + function nextGuryId() { + return "gury_id_" + (guryId++); + } + + function getGury(canvas) { + if (!isString(canvas._gury_id) || !(canvasToGury[canvas._gury_id] instanceof Gury)) { + return null; + } + return canvasToGury[canvas._gury_id]; + } + + function setGury(canvas, gury) { + if (typeof canvas._gury_id == "string") { + gury.id = canvas._gury_id; + } + else { + gury.id = canvas._gury_id = nextGuryId(); + } + + return canvasToGury[gury.id] = gury; + } + + /* + * Tag Namespace Object + */ + function TagSpace(name, objects) { + this.name = name; + this._children = {}; + this._objects = objects || []; + } + + TagSpace.TAG_REGEX = /^[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$/; + + TagSpace.prototype.hasChild = function(name) { + return isObject(this._children[name]); + }; + + TagSpace.prototype.addChild = function(name) { + return this._children[name] = new TagSpace(name); + }; + + TagSpace.prototype.getChild = function(name) { + return this._children[name]; + }; + + TagSpace.prototype.getObjects = function() { + // This might be a little slow, but it helps us keep spaces consistent + var objects = []; + for (var i = 0; i < this._objects.length; i++) { + objects.push(this._objects[i]); + } + + // And lets us annotate what we return :) + objects.each = _each; + + return objects; + }; + + TagSpace.prototype.find = function(tag) { + if (!tag.match(TagSpace.TAG_REGEX)) { + return null; + } + + var currentSpace = this; + var tags = tag.split('.'); + var lastName = tags[tags.length - 1]; + + for (var i = 0; i < tags.length; i++) { + if (!currentSpace.hasChild(tags[i])) + return null; + currentSpace = currentSpace.getChild(tags[i]); + } + + return currentSpace; + }; + + TagSpace.prototype.add = function(tag, object) { + if (!tag.match(TagSpace.TAG_REGEX)) { + return null; + } + + var currentSpace = this; + var tags = tag.split('.'); + var lastName = tags[tags.length - 1]; + + for (var i = 0; i < tags.length; i++) { + if (currentSpace.hasChild(tags[i])) { + currentSpace = currentSpace.getChild(tags[i]); + } + else { + currentSpace = currentSpace.addChild(tags[i]); + } + } + + // TODO: There's probably a better way to check for duplicates + // ... but that's why they call it "iterative" development :P + + for (i = 0; i < currentSpace._objects.length; i++) { + if (currentSpace._objects[i] == object) { + return object; + } + } + + currentSpace._objects.push(object); + + return object; + }; + + /* + * Core Gury Class + */ + + function Gury(canvas) { + if (canvas == null) { + canvas = document.createElement('canvas'); + } + + // Check for an existing mapping from the canvas to a Gury instance + if (getGury(canvas)) { + return getGury(canvas); + } + + // Otherwise create a new instance + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + + this._objects = []; + this._objects.each = _each; + + this._tags = new TagSpace('__global'); + + this._paused = false; + this._loop_interval = null; + + return setGury(canvas, this); + } + + Gury.prototype.place = function(node) { + if (typeof node == "string" && typeof $ == "function") { + $(node).append(this.canvas); + } + else if (typeof node == "object" && typeof node.addChild == "function") { + node.addChild(this.canvas); + } + else { + GuryException("place() - Unable to place canvas tag (is jQuery loaded?)"); + } + return this; + }; + + /* + * Canvas style methods + */ + + Gury.prototype.size = function(w, h) { + this.canvas.width = w; + this.canvas.height = h; + return this; + }; + + Gury.prototype.background = function(bg) { + this.canvas.style.background = bg; + return this; + }; + + /* + * Objects and Rendering + */ + + function _annotate_object(object) { + object._gury = { + visible: true + }; + } + + Gury.prototype.add = function() { + var tag = null, obj; + + if (arguments.length < 1) { + return this; + } + else if (arguments.length < 2) { + obj = arguments[0]; + if (!isObjectOrFunction(obj)) { + return this; + } + } + else { + tag = arguments[0]; + obj = arguments[1]; + if (!isString(name) || !isObjectOrFunction(obj)) { + return this; + } + } + + // Annotate the object with gury specific members + _annotate_object(obj); + + // Add the object to the global tag space (if a tag was provided) + if (tag != null) { + this._tags.add(tag, obj); + } + + // Add to the rendering list + this._objects.push(obj); + + return this; + }; + + Gury.prototype.clear = function() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + return this; + }; + + Gury.prototype.draw = function() { + this.clear(); + + for (var i = 0; i < this._objects.length; i++) { + var ob = this._objects[i]; + + if (!ob._gury.visible) { + continue; + } + + if (typeof ob == "function") { + ob.call(this, this.ctx); + } + else if (typeof ob == "object" && typeof ob.draw != "undefined") { + ob.draw(this.ctx, this.canvas); + } + } + return this; + }; + + /* + * Animation Controls + */ + + Gury.prototype.play = function(interval) { + // Ignore multiple play attempts + if (this._loop_interval != null) { + return this; + } + + var _gury = this; + this._loop_interval = setInterval(function() { + if (!_gury._paused) { + _gury.draw(); + } + }, interval); + return this; + }; + + Gury.prototype.pause = function() { + this._paused = !this._paused; + return this; + }; + + Gury.prototype.stop = function() { + if (this._loop_interval != null) { + clearInterval(this._loop_interval); + this._paused = false; + } + return this; + }; + + /* + * Object / Tag Methods + */ + Gury.prototype.each = function() { + var tag, closure; + + if (arguments.length < 2 && isFunction(arguments[0])) { + closure = arguments[0]; + this._objects.each(closure); + } + else if (isString(arguments[0]) && isFunction(arguments[1])) { + tag = arguments[0]; + closure = arguments[1]; + var space = this._tags.find(tag); + if (space) { + space.getObjects().each(closure); + } + } + else if (isFunction(arguments[0])) { + closure = arguments[0]; + this._objects.each(closure); + } + else if (isFunction(arguments[1])) { + closure = arguments[1]; + this._objects.each(closure); + } + + return this; + }; + + Gury.prototype.hide = function(tag) { + return this.each(tag, function(obj, index) { + obj._gury.visible = false; + }); + }; + + Gury.prototype.show = function(tag) { + return this.each(tag, function(obj, index) { + obj._gury.visible = true; + }); + }; + + Gury.prototype.toggle = function(tag) { + return this.each(tag, function(obj, index) { + obj._gury.visible = !obj._gury.visible; + }); + }; + + /* + * Public interface + */ + + function GuryInterface(id) { + return new Gury(id ? document.getElementById(id) : null); + } + + GuryInterface.failWithException = function(b) { + if (!b) { + return _failWithException; + } + return _failWithException = b ? true : false; + }; + + return GuryInterface; +})(); + +// "There's a star man waiting in the sky. He'd like to come and meet us but +// he think's he'll blow our minds." \ No newline at end of file diff --git a/lib/jquery-1.4.3.js b/lib/jquery-1.4.3.js new file mode 100644 index 0000000..ad9a79c --- /dev/null +++ b/lib/jquery-1.4.3.js @@ -0,0 +1,6883 @@ +/*! + * jQuery JavaScript Library v1.4.3 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Oct 14 23:10:06 2010 -0400 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, + + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + rwhite = /\s/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Check for non-word characters + rnonword = /\W/, + + // Check for digits + rdigit = /\d/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // Has the ready events already been bound? + readyBound = false, + + // The functions to execute on DOM ready + readyList = [], + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $("TAG") + } else if ( !context && !rnonword.test( selector ) ) { + this.selector = selector; + this.context = document; + selector = document.getElementsByTagName( selector ); + return jQuery.merge( this, selector ); + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return (context || rootjQuery).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return jQuery( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if (selector.selector !== undefined) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.4.3", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + (this.selector ? " " : "") + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // If the DOM is already ready + if ( jQuery.isReady ) { + // Execute the function immediately + fn.call( document, jQuery ); + + // Otherwise, remember the function for later + } else if ( readyList ) { + // Add the function to the wait list + readyList.push( fn ); + } + + return this; + }, + + eq: function( i ) { + return i === -1 ? + this.slice( i ) : + this.slice( i, +i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || jQuery(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy, copyIsArray; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + // A third-party is pushing the ready event forwards + if ( wait === true ) { + jQuery.readyWait--; + } + + // Make sure that the DOM is not already loaded + if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + if ( readyList ) { + // Execute all of them + var fn, i = 0; + while ( (fn = readyList[ i++ ]) ) { + fn.call( document, jQuery ); + } + + // Reset the list of functions + readyList = null; + } + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyBound ) { + return; + } + + readyBound = true; + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNaN: function( obj ) { + return obj == null || !rdigit.test( obj ) || isNaN( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw msg; + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test(data.replace(rvalidescape, "@") + .replace(rvalidtokens, "]") + .replace(rvalidbraces, "")) ) { + + // Try to use the native JSON parser first + return window.JSON && window.JSON.parse ? + window.JSON.parse( data ) : + (new Function("return " + data))(); + + } else { + jQuery.error( "Invalid JSON: " + data ); + } + }, + + noop: function() {}, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && rnotwhite.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + + if ( jQuery.support.scriptEval ) { + script.appendChild( document.createTextNode( data ) ); + } else { + script.text = data; + } + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction(object); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // The extra typeof function check is to prevent crashes + // in Safari 2 (See: #3039) + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type(array); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var ret = [], value; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, length = elems.length; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + proxy: function( fn, proxy, thisObject ) { + if ( arguments.length === 2 ) { + if ( typeof proxy === "string" ) { + thisObject = fn; + fn = thisObject[ proxy ]; + proxy = undefined; + + } else if ( proxy && !jQuery.isFunction( proxy ) ) { + thisObject = proxy; + proxy = undefined; + } + } + + if ( !proxy && fn ) { + proxy = function() { + return fn.apply( thisObject || this, arguments ); + }; + } + + // Set the guid of unique handler to the same of original handler, so it can be removed + if ( fn ) { + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + } + + // So proxy can be declared as an argument + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can be optionally by executed if its a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return (new Date()).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +if ( indexOf ) { + jQuery.inArray = function( elem, array ) { + return indexOf.call( array, elem ); + }; +} + +// Verify that \s matches non-breaking spaces +// (IE fails on this test) +if ( !rwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +// Expose jQuery to the global object +return (window.jQuery = window.$ = jQuery); + +})(); + + +(function() { + + jQuery.support = {}; + + var root = document.documentElement, + script = document.createElement("script"), + div = document.createElement("div"), + id = "script" + jQuery.now(); + + div.style.display = "none"; + div.innerHTML = "
a"; + + var all = div.getElementsByTagName("*"), + a = div.getElementsByTagName("a")[0], + select = document.createElement("select"), + opt = select.appendChild( document.createElement("option") ); + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return; + } + + jQuery.support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText insted) + style: /red/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55$/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: div.getElementsByTagName("input")[0].value === "on", + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Will be defined later + optDisabled: false, + checkClone: false, + scriptEval: false, + noCloneEvent: true, + boxModel: null, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableHiddenOffsets: true + }; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as diabled) + select.disabled = true; + jQuery.support.optDisabled = !opt.disabled; + + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} + + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support.scriptEval = true; + delete window[ id ]; + } + + root.removeChild( script ); + + if ( div.attachEvent && div.fireEvent ) { + div.attachEvent("onclick", function click() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + jQuery.support.noCloneEvent = false; + div.detachEvent("onclick", click); + }); + div.cloneNode(true).fireEvent("onclick"); + } + + div = document.createElement("div"); + div.innerHTML = ""; + + var fragment = document.createDocumentFragment(); + fragment.appendChild( div.firstChild ); + + // WebKit doesn't clone checked state correctly in fragments + jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + + // Figure out if the W3C box model works as expected + // document.body must exist before we can do this + jQuery(function() { + var div = document.createElement("div"); + div.style.width = div.style.paddingLeft = "1px"; + + document.body.appendChild( div ); + jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + + if ( "zoom" in div.style ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
"; + jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; + } + + div.innerHTML = "
t
"; + var tds = div.getElementsByTagName("td"); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + + tds[0].style.display = ""; + tds[1].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE < 8 fail this test) + jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; + div.innerHTML = ""; + + document.body.removeChild( div ).style.display = "none"; + div = tds = null; + }); + + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); + + // release memory in IE + root = script = div = all = a = null; +})(); + +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; + + + + +var windowData = {}, + rbrace = /^(?:\{.*\}|\[.*\])$/; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + expando: "jQuery" + jQuery.now(), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + data: function( elem, name, data ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + elem = elem == window ? + windowData : + elem; + + var isNode = elem.nodeType, + id = isNode ? elem[ jQuery.expando ] : null, + cache = jQuery.cache, thisCache; + + if ( isNode && !id && typeof name === "string" && data === undefined ) { + return; + } + + // Get the data from the object directly + if ( !isNode ) { + cache = elem; + + // Compute a unique ID for the element + } else if ( !id ) { + elem[ jQuery.expando ] = id = ++jQuery.uuid; + } + + // Avoid generating a new cache unless none exists and we + // want to manipulate it. + if ( typeof name === "ob