From: dsc Date: Sat, 30 Oct 2010 23:54:53 +0000 (-0700) Subject: Initial commit X-Git-Url: http://git.lttlst.com:3516/?a=commitdiff_plain;h=9354f69ddac5b9e1c1b3d8e47b4367540606efd2;p=tanks.git Initial commit --- 9354f69ddac5b9e1c1b3d8e47b4367540606efd2 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 === "object" ) { + if ( isNode ) { + cache[ id ] = jQuery.extend(cache[ id ], name); + + } else { + jQuery.extend( cache, name ); + } + + } else if ( isNode && !cache[ id ] ) { + cache[ id ] = {}; + } + + thisCache = isNode ? cache[ id ] : cache; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) { + thisCache[ name ] = data; + } + + return typeof name === "string" ? thisCache[ name ] : thisCache; + }, + + removeData: function( elem, name ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + elem = elem == window ? + windowData : + elem; + + var isNode = elem.nodeType, + id = isNode ? elem[ jQuery.expando ] : elem, + cache = jQuery.cache, + thisCache = isNode ? cache[ id ] : id; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( thisCache ) { + // Remove the section of cache data + delete thisCache[ name ]; + + // If we've removed all the data, remove the element's cache + if ( isNode && jQuery.isEmptyObject(thisCache) ) { + jQuery.removeData( elem ); + } + } + + // Otherwise, we want to remove all of the element's data + } else { + if ( isNode && jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + + // Completely remove the data cache + } else if ( isNode ) { + delete cache[ id ]; + + // Remove all fields from the object + } else { + for ( var n in elem ) { + delete elem[ n ]; + } + } + } + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + if ( typeof key === "undefined" ) { + return this.length ? jQuery.data( this[0] ) : null; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && this[0].nodeType === 1 ) { + data = this[0].getAttribute( "data-" + key ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + !jQuery.isNaN( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + } else { + data = undefined; + } + } + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var $this = jQuery( this ), args = [ parts[0], value ]; + + $this.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + $this.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + + + + +jQuery.extend({ + queue: function( elem, type, data ) { + if ( !elem ) { + return; + } + + type = (type || "fx") + "queue"; + var q = jQuery.data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( !data ) { + return q || []; + } + + if ( !q || jQuery.isArray(data) ) { + q = jQuery.data( elem, type, jQuery.makeArray(data) ); + + } else { + q.push( data ); + } + + return q; + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), fn = queue.shift(); + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift("inprogress"); + } + + fn.call(elem, function() { + jQuery.dequeue(elem, type); + }); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function( i ) { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + type = type || "fx"; + + return this.queue( type, function() { + var elem = this; + setTimeout(function() { + jQuery.dequeue( elem, type ); + }, time ); + }); + }, + + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + } +}); + + + + +var rclass = /[\n\t]/g, + rspaces = /\s+/, + rreturn = /\r/g, + rspecialurl = /^(?:href|src|style)$/, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rradiocheck = /^(?:radio|checkbox)$/i; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name, fn ) { + return this.each(function(){ + jQuery.attr( this, name, "" ); + if ( this.nodeType === 1 ) { + this.removeAttribute( name ); + } + }); + }, + + addClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.addClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( value && typeof value === "string" ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className ) { + elem.className = value; + + } else { + var className = " " + elem.className + " ", setClass = elem.className; + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { + setClass += " " + classNames[c]; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.removeClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + var className = (" " + elem.className + " ").replace(rclass, " "); + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[c] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this); + self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, i = 0, self = jQuery(this), + state = stateVal, + classNames = value.split( rspaces ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery.data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " "; + for ( var i = 0, l = this.length; i < l; i++ ) { + if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + if ( !arguments.length ) { + var elem = this[0]; + + if ( elem ) { + if ( jQuery.nodeName( elem, "option" ) ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { + return elem.getAttribute("value") === null ? "on" : elem.value; + } + + + // Everything else, we just grab the value + return (elem.value || "").replace(rreturn, ""); + + } + + return undefined; + } + + var isFunction = jQuery.isFunction(value); + + return this.each(function(i) { + var self = jQuery(this), val = value; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call(this, i, self.val()); + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray(val) ) { + val = jQuery.map(val, function (value) { + return value == null ? "" : value + ""; + }); + } + + if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { + this.checked = jQuery.inArray( self.val(), val ) >= 0; + + } else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(val); + + jQuery( "option", this ).each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + this.selectedIndex = -1; + } + + } else { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + // don't set attributes on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery(elem)[name](value); + } + + var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), + // Whether we are setting (or getting) + set = value !== undefined; + + // Try to normalize/fix the name + name = notxml && jQuery.props[ name ] || name; + + // Only do all the following if this is a node (faster for style) + if ( elem.nodeType === 1 ) { + // These attributes require special treatment + var special = rspecialurl.test( name ); + + // Safari mis-reports the default selected property of an option + // Accessing the parent's selectedIndex property fixes it + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + + // If applicable, access the attribute via the DOM 0 way + // 'in' checks fail in Blackberry 4.7 #6931 + if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { + if ( set ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } + + if ( value === null ) { + if ( elem.nodeType === 1 ) { + elem.removeAttribute( name ); + } + + } else { + elem[ name ] = value; + } + } + + // browsers index elements by id/name on forms, give priority to attributes. + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { + return elem.getAttributeNode( name ).nodeValue; + } + + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + if ( name === "tabIndex" ) { + var attributeNode = elem.getAttributeNode( "tabIndex" ); + + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + + return elem[ name ]; + } + + if ( !jQuery.support.style && notxml && name === "style" ) { + if ( set ) { + elem.style.cssText = "" + value; + } + + return elem.style.cssText; + } + + if ( set ) { + // convert the value to a string (all browsers do this but IE) see #1070 + elem.setAttribute( name, "" + value ); + } + + // Ensure that missing attributes return undefined + // Blackberry 4.7 returns "" from getAttribute #6938 + if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { + return undefined; + } + + var attr = !jQuery.support.hrefNormalized && notxml && special ? + // Some attributes require a special call on IE + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return attr === null ? undefined : attr; + } + } +}); + + + + +var rnamespaces = /\.(.*)$/, + rformElems = /^(?:textarea|input|select)$/i, + rperiod = /\./g, + rspace = / /g, + rescape = /[^\w\s.|`]/g, + fcleanup = function( nm ) { + return nm.replace(rescape, "\\$&"); + }, + focusCounts = { focusin: 0, focusout: 0 }; + +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code originated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { + + // Bind an event to an element + // Original by Dean Edwards + add: function( elem, types, handler, data ) { + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { + elem = window; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var handleObjIn, handleObj; + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure + var elemData = jQuery.data( elem ); + + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; + } + + // Use a key less likely to result in collisions for plain JS objects. + // Fixes bug #7150. + var eventKey = elem.nodeType ? "events" : "__events__", + events = elemData[ eventKey ], + eventHandle = elemData.handle; + + if ( typeof events === "function" ) { + // On plain objects events is a fn that holds the the data + // which prevents this data from being JSON serialized + // the function does not need to be called, it just contains the data + eventHandle = events.handle; + events = events.events; + + } else if ( !events ) { + if ( !elem.nodeType ) { + // On plain objects, create a fn that acts as the holder + // of the values to avoid JSON serialization of event data + elemData[ eventKey ] = elemData = function(){}; + } + + elemData.events = events = {}; + } + + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + return typeof jQuery !== "undefined" && !jQuery.event.triggered ? + jQuery.event.handle.apply( eventHandle.elem, arguments ) : + undefined; + }; + } + + // Add elem as a property of the handle function + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = types.split(" "); + + var type, i = 0, namespaces; + + while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + + // Namespaced event handlers + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); + + } else { + namespaces = []; + handleObj.namespace = ""; + } + + handleObj.type = type; + if ( !handleObj.guid ) { + handleObj.guid = handler.guid; + } + + // Get the current list of functions bound to this event + var handlers = events[ type ], + special = jQuery.event.special[ type ] || {}; + + // Init the event handler queue + if ( !handlers ) { + handlers = events[ type ] = []; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add the function to the element's handler list + handlers.push( handleObj ); + + // Keep track of which events have been used, for global triggering + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, pos ) { + // don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + eventKey = elem.nodeType ? "events" : "__events__", + elemData = jQuery.data( elem ), + events = elemData && elemData[ eventKey ]; + + if ( !elemData || !events ) { + return; + } + + if ( typeof events === "function" ) { + elemData = events; + events = events.events; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } + } + + continue; + } + + special = jQuery.event.special[ type ] || {}; + + for ( j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); + } + + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + + if ( pos != null ) { + break; + } + } + } + + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( typeof elemData === "function" ) { + jQuery.removeData( elem, eventKey ); + + } else if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem ); + } + } + }, + + // bubbling is internal + trigger: function( event, data, elem /*, bubbling */ ) { + // Event object or event type + var type = event.type || event, + bubbling = arguments[3]; + + if ( !bubbling ) { + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + jQuery.extend( jQuery.Event(type), event ) : + // Just the event type (string) + jQuery.Event(type); + + if ( type.indexOf("!") >= 0 ) { + event.type = type = type.slice(0, -1); + event.exclusive = true; + } + + // Handle a global trigger + if ( !elem ) { + // Don't bubble custom events when global (to avoid too much overhead) + event.stopPropagation(); + + // Only trigger if we've ever bound an event for it + if ( jQuery.event.global[ type ] ) { + jQuery.each( jQuery.cache, function() { + if ( this.events && this.events[type] ) { + jQuery.event.trigger( event, data, this.handle.elem ); + } + }); + } + } + + // Handle triggering a single element + + // don't do events on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + // Clean up in case it is reused + event.result = undefined; + event.target = elem; + + // Clone the incoming data, if any + data = jQuery.makeArray( data ); + data.unshift( event ); + } + + event.currentTarget = elem; + + // Trigger the event, it is assumed that "handle" is a function + var handle = elem.nodeType ? + jQuery.data( elem, "handle" ) : + (jQuery.data( elem, "__events__" ) || {}).handle; + + if ( handle ) { + handle.apply( elem, data ); + } + + var parent = elem.parentNode || elem.ownerDocument; + + // Trigger an inline bound script + try { + if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { + if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { + event.result = false; + event.preventDefault(); + } + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (inlineError) {} + + if ( !event.isPropagationStopped() && parent ) { + jQuery.event.trigger( event, data, parent, true ); + + } else if ( !event.isDefaultPrevented() ) { + var target = event.target, old, targetType = type.replace(rnamespaces, ""), + isClick = jQuery.nodeName(target, "a") && targetType === "click", + special = jQuery.event.special[ targetType ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + + try { + if ( target[ targetType ] ) { + // Make sure that we don't accidentally re-trigger the onFOO events + old = target[ "on" + targetType ]; + + if ( old ) { + target[ "on" + targetType ] = null; + } + + jQuery.event.triggered = true; + target[ targetType ](); + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (triggerError) {} + + if ( old ) { + target[ "on" + targetType ] = old; + } + + jQuery.event.triggered = false; + } + } + }, + + handle: function( event ) { + var all, handlers, namespaces, namespace_sort = [], namespace_re, events, args = jQuery.makeArray( arguments ); + + event = args[0] = jQuery.event.fix( event || window.event ); + event.currentTarget = this; + + // Namespaced event handlers + all = event.type.indexOf(".") < 0 && !event.exclusive; + + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace_sort = namespaces.slice(0).sort(); + namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.namespace = event.namespace || namespace_sort.join("."); + + events = jQuery.data(this, this.nodeType ? "events" : "__events__"); + + if ( typeof events === "function" ) { + events = events.events; + } + + handlers = (events || {})[ event.type ]; + + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + + // Filter the functions by class + if ( all || namespace_re.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + } + + return event.result; + }, + + props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // store a copy of the original event object + // and "clone" to set read-only properties + var originalEvent = event; + event = jQuery.Event( originalEvent ); + + for ( var i = this.props.length, prop; i; ) { + prop = this.props[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary + if ( !event.target ) { + event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + } + + // check if target is a textnode (safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var doc = document.documentElement, body = document.body; + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Add which for key events + if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { + event.which = event.charCode != null ? event.charCode : event.keyCode; + } + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) { + event.metaKey = event.ctrlKey; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button !== undefined ) { + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } + + return event; + }, + + // Deprecated, use jQuery.guid instead + guid: 1E8, + + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady, + teardown: jQuery.noop + }, + + live: { + add: function( handleObj ) { + jQuery.event.add( this, + liveConvert( handleObj.origType, handleObj.selector ), + jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); + }, + + remove: function( handleObj ) { + jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); + } + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src ) { + // Allow instantiation without the 'new' keyword + if ( !this.preventDefault ) { + return new jQuery.Event( src ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + // Event type + } else { + this.type = src; + } + + // timeStamp is buggy for some events on Firefox(#3843) + // So we won't rely on the native value + this.timeStamp = jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function( event ) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { + parent = parent.parentNode; + } + + if ( parent !== this ) { + // set the correct event type + event.type = event.data; + + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } +}, + +// In case of event delegation, we only need to rename the event.type, +// liveHandler will take care of the rest. +delegate = function( event ) { + event.type = event.data; + jQuery.event.handle.apply( this, arguments ); +}; + +// Create mouseenter and mouseleave events +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + setup: function( data ) { + jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); + }, + teardown: function( data ) { + jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + } + }; +}); + +// submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); + } + }; + +} + +// change delegation, happens here so we have bind. +if ( !jQuery.support.changeBubbles ) { + + var changeFilters, + + getVal = function( elem ) { + var type = elem.type, val = elem.value; + + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; + + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; + + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; + } + + return val; + }, + + testChange = function testChange( e ) { + var elem = e.target, data, val; + + if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { + return; + } + + data = jQuery.data( elem, "_change_data" ); + val = getVal(elem); + + // the current data will be also retrieved by beforeactivate + if ( e.type !== "focusout" || elem.type !== "radio" ) { + jQuery.data( elem, "_change_data", val ); + } + + if ( data === undefined || val === data ) { + return; + } + + if ( data != null || val ) { + e.type = "change"; + e.liveFired = undefined; + return jQuery.event.trigger( e, arguments[1], elem ); + } + }; + + jQuery.event.special.change = { + filters: { + focusout: testChange, + + beforedeactivate: testChange, + + click: function( e ) { + var elem = e.target, type = elem.type; + + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, + + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; + + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, + + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information + beforeactivate: function( e ) { + var elem = e.target; + jQuery.data( elem, "_change_data", getVal(elem) ); + } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return rformElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return rformElems.test( this.nodeName ); + } + }; + + changeFilters = jQuery.event.special.change.filters; + + // Handle when the input is .focus()'d + changeFilters.focus = changeFilters.beforeactivate; +} + +function trigger( type, elem, args ) { + args[0].type = type; + return jQuery.event.handle.apply( elem, args ); +} + +// Create "bubbling" focus and blur events +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + if ( focusCounts[fix]++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --focusCounts[fix] === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.trigger( e, null, e.target ); + } + }); +} + +jQuery.each(["bind", "one"], function( i, name ) { + jQuery.fn[ name ] = function( type, data, fn ) { + // Handle object literals + if ( typeof type === "object" ) { + for ( var key in type ) { + this[ name ](key, data, type[key], fn); + } + return this; + } + + if ( jQuery.isFunction( data ) || data === false ) { + fn = data; + data = undefined; + } + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }) : fn; + + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; + }; +}); + +jQuery.fn.extend({ + unbind: function( type, fn ) { + // Handle object literals + if ( typeof type === "object" && !type.preventDefault ) { + for ( var key in type ) { + this.unbind(key, type[key]); + } + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } + } + + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + + triggerHandler: function( type, data ) { + if ( this[0] ) { + var event = jQuery.Event( type ); + event.preventDefault(); + event.stopPropagation(); + jQuery.event.trigger( event, data, this[0] ); + return event.result; + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, i = 1; + + // link all the functions, so any of them can unbind this click handler + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); + } + + return this.click( jQuery.proxy( fn, function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + })); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + +jQuery.each(["live", "die"], function( i, name ) { + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); + + if ( typeof types === "object" && !types.preventDefault ) { + for ( var key in types ) { + context[ name ]( key, data, types[key], selector ); + } + + return this; + } + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + types = (types || "").split(" "); + + while ( (type = types[ i++ ]) != null ) { + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + + if ( name === "live" ) { + // bind live handler + for ( var j = 0, l = context.length; j < l; j++ ) { + jQuery.event.add( context[j], "live." + liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + } + + } else { + // unbind live handler + context.unbind( "live." + liveConvert( type, selector ), fn ); + } + } + + return this; + }; +}); + +function liveHandler( event ) { + var stop, maxLevel, elems = [], selectors = [], + related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + events = jQuery.data( this, this.nodeType ? "events" : "__events__" ); + + if ( typeof events === "function" ) { + events = events.events; + } + + // Make sure we avoid non-left-click bubbling in Firefox (#3861) + if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { + return; + } + + if ( event.namespace ) { + namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); + + } else { + live.splice( j--, 1 ); + } + } + + match = jQuery( event.target ).closest( selectors, event.currentTarget ); + + for ( i = 0, l = match.length; i < l; i++ ) { + close = match[i]; + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { + elem = close.elem; + related = null; + + // Those two events require additional checking + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + event.type = handleObj.preType; + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; + } + + if ( !related || related !== elem ) { + elems.push({ elem: elem, handleObj: handleObj, level: close.level }); + } + } + } + } + + for ( i = 0, l = elems.length; i < l; i++ ) { + match = elems[i]; + + if ( maxLevel && match.level > maxLevel ) { + break; + } + + event.currentTarget = match.elem; + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + ret = match.handleObj.origHandler.apply( match.elem, arguments ); + + if ( ret === false || event.isPropagationStopped() ) { + maxLevel = match.level; + + if ( ret === false ) { + stop = false; + } + } + } + + return stop; +} + +function liveConvert( type, selector ) { + return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); +} + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.bind( name, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } +}); + +// Prevent memory leaks in IE +// Window isn't included so as not to unbind existing unload events +// More info: +// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ +if ( window.attachEvent && !window.addEventListener ) { + jQuery(window).bind("unload", function() { + for ( var id in jQuery.cache ) { + if ( jQuery.cache[ id ].handle ) { + // Try/Catch is to handle iframes being unloaded, see #4280 + try { + jQuery.event.remove( jQuery.cache[ id ].handle.elem ); + } catch(e) {} + } + } + }); +} + + +/*! + * Sizzle CSS Selector Engine - v1.0 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function(){ + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function(selector, context, results, seed) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), + soFar = selector, ret, cur, pop, i; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec(""); + m = chunker.exec(soFar); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set ); + } + } + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray(set); + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); + } + } + } + } + + return results; +}; + +Sizzle.matches = function(expr, set){ + return Sizzle(expr, null, null, set); +}; + +Sizzle.matchesSelector = function(node, expr){ + return Sizzle(expr, null, null, [node]).length > 0; +}; + +Sizzle.find = function(expr, context, isXML){ + var set; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var type = Expr.order[i], match; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice(1,1); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = context.getElementsByTagName("*"); + } + + return {set: set, expr: expr}; +}; + +Sizzle.filter = function(expr, set, inplace, not){ + var old = expr, result = [], curLoop = set, match, anyFound, + isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var filter = Expr.filter[ type ], found, item, left = match[1]; + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + } else { + curLoop[i] = false; + } + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + leftMatch: {}, + attrMap: { + "class": "className", + "for": "htmlFor" + }, + attrHandle: { + href: function(elem){ + return elem.getAttribute("href"); + } + }, + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test(part), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + ">": function(checkSet, part){ + var isPartStr = typeof part === "string", + elem, i = 0, l = checkSet.length; + + if ( isPartStr && !/\W/.test(part) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + "": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + }, + "~": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + } + }, + find: { + ID: function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + NAME: function(match, context){ + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], results = context.getElementsByName(match[1]); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + TAG: function(match, context){ + return context.getElementsByTagName(match[1]); + } + }, + preFilter: { + CLASS: function(match, curLoop, inplace, result, not, isXML){ + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + ID: function(match){ + return match[1].replace(/\\/g, ""); + }, + TAG: function(match, curLoop){ + return match[1].toLowerCase(); + }, + CHILD: function(match){ + if ( match[1] === "nth" ) { + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + ATTR: function(match, curLoop, inplace, result, not, isXML){ + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + PSEUDO: function(match, curLoop, inplace, result, not){ + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { + result.push.apply( result, ret ); + } + return false; + } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + POS: function(match){ + match.unshift( true ); + return match; + } + }, + filters: { + enabled: function(elem){ + return elem.disabled === false && elem.type !== "hidden"; + }, + disabled: function(elem){ + return elem.disabled === true; + }, + checked: function(elem){ + return elem.checked === true; + }, + selected: function(elem){ + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + return elem.selected === true; + }, + parent: function(elem){ + return !!elem.firstChild; + }, + empty: function(elem){ + return !elem.firstChild; + }, + has: function(elem, i, match){ + return !!Sizzle( match[3], elem ).length; + }, + header: function(elem){ + return (/h\d/i).test( elem.nodeName ); + }, + text: function(elem){ + return "text" === elem.type; + }, + radio: function(elem){ + return "radio" === elem.type; + }, + checkbox: function(elem){ + return "checkbox" === elem.type; + }, + file: function(elem){ + return "file" === elem.type; + }, + password: function(elem){ + return "password" === elem.type; + }, + submit: function(elem){ + return "submit" === elem.type; + }, + image: function(elem){ + return "image" === elem.type; + }, + reset: function(elem){ + return "reset" === elem.type; + }, + button: function(elem){ + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + input: function(elem){ + return (/input|select|textarea|button/i).test(elem.nodeName); + } + }, + setFilters: { + first: function(elem, i){ + return i === 0; + }, + last: function(elem, i, match, array){ + return i === array.length - 1; + }, + even: function(elem, i){ + return i % 2 === 0; + }, + odd: function(elem, i){ + return i % 2 === 1; + }, + lt: function(elem, i, match){ + return i < match[3] - 0; + }, + gt: function(elem, i, match){ + return i > match[3] - 0; + }, + nth: function(elem, i, match){ + return match[3] - 0 === i; + }, + eq: function(elem, i, match){ + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function(elem, match, i, array){ + var name = match[1], filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + } else { + Sizzle.error( "Syntax error, unrecognized expression: " + name ); + } + }, + CHILD: function(elem, match){ + var type = match[1], node = elem; + switch (type) { + case 'only': + case 'first': + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + if ( type === "first" ) { + return true; + } + node = elem; + case 'last': + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + return true; + case 'nth': + var first = match[2], last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + if ( first === 0 ) { + return diff === 0; + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + ID: function(elem, match){ + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + TAG: function(elem, match){ + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + }, + CLASS: function(elem, match){ + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + ATTR: function(elem, match){ + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + POS: function(elem, match, i, array){ + var name = match[2], filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function(array, results) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch(e){ + makeArray = function(array, results) { + var ret = results || [], i = 0; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; +} else { + sortOrder = function( a, b ) { + var ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, + cur = aup, al, bl; + + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // If the nodes are siblings (or identical) we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } + + return ret; +}; + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(); + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + var root = document.documentElement; + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + } + }; + + Expr.filter.ID = function(elem, match){ + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + root = form = null; // release memory in IE +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function(match, context){ + var results = context.getElementsByTagName(match[1]); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + Expr.attrHandle.href = function(elem){ + return elem.getAttribute("href", 2); + }; + } + + div = null; // release memory in IE +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function(query, context, extra, seed){ + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + if ( context.nodeType === 9 ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var old = context.id, id = context.id = "__sizzle__"; + + try { + return makeArray( context.querySelectorAll( "#" + id + " " + query ), extra ); + + } catch(pseudoError) { + } finally { + if ( old ) { + context.id = old; + + } else { + context.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + div = null; // release memory in IE + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, ":sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + if ( matches ) { + Sizzle.matchesSelector = function( node, expr ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) ) { + return matches.call( node, expr ); + } + } catch(e) {} + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function(match, context, isXML) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + div = null; // release memory in IE +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +Sizzle.contains = document.documentElement.contains ? function(a, b){ + return a !== b && (a.contains ? a.contains(b) : true); +} : function(a, b){ + return !!(a.compareDocumentPosition(b) & 16); +}; + +Sizzle.isXML = function(elem){ + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function(selector, context){ + var tmpSet = [], later = "", match, + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS; + +jQuery.fn.extend({ + find: function( selector ) { + var ret = this.pushStack( "", "find", selector ), length = 0; + + for ( var i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( var n = length; n < ret.length; n++ ) { + for ( var r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && jQuery.filter( selector, this ).length > 0; + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + if ( jQuery.isArray( selectors ) ) { + var match, matches = {}, selector, level = 1; + + if ( cur && selectors.length ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { + selector = selectors[i]; + + if ( !matches[selector] ) { + matches[selector] = jQuery.expr.match.POS.test( selector ) ? + jQuery( selector, context || this.context ) : + selector; + } + } + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( selector in matches ) { + match = matches[selector]; + + if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { + ret.push({ selector: selector, elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + } + + return ret; + } + + var pos = POS.test( selectors ) ? + jQuery( selectors, context || this.context ) : null; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique(ret) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + if ( !elem || typeof elem === "string" ) { + return jQuery.inArray( this[0], + // If it receives a string, the selector is used + // If it receives nothing, the siblings are used + elem ? jQuery( elem ) : this.parent().children() ); + } + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context || this.context ) : + jQuery.makeArray( selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call(arguments).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], cur = elem[dir]; + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return (elem === qualifier) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + }); +} + + + + +var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /\s]+\/)>/g, + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and \n"; + } +} + +function y_dump($expose=false) { + global $y_files; + if (!$expose) + dump_file("./_intro.js"); + foreach ($y_files as $f) + dump_file("./$f.js"); + if (!$expose) + dump_file("./_outro.js"); +} + +if ( basename($_SERVER["SCRIPT_FILENAME"]) == basename(__FILE__) ) { + if ( $_REQUEST["list"] ) + y_list(); + else + y_dump(); +} \ No newline at end of file diff --git a/src/js/_intro.js b/src/js/_intro.js new file mode 100644 index 0000000..599c5c9 --- /dev/null +++ b/src/js/_intro.js @@ -0,0 +1,7 @@ +(function(){ + +var undefined, +globals = this, +_toString = _Object.prototype.toString, +_hasOwn = _Object.prototype.hasOwnProperty, +_isArray = _Array.isArray; diff --git a/src/js/_outro.js b/src/js/_outro.js new file mode 100644 index 0000000..efe95da --- /dev/null +++ b/src/js/_outro.js @@ -0,0 +1,6 @@ + +globals.reduce = reduce; +globals.extend = extend; +globals.attr = attr; + +})(); diff --git a/src/js/array.js b/src/js/array.js new file mode 100644 index 0000000..e69de29 diff --git a/src/js/core.js b/src/js/core.js new file mode 100644 index 0000000..40c493e --- /dev/null +++ b/src/js/core.js @@ -0,0 +1,39 @@ +// Generic Collection Functions + +function notSelfOrWrapped(fn){ + var self = arguments.callee.caller; + return fn && fn !== self && fn.__wraps !== self; +} + +function reduce(o, fn, acc, cxt){ + if ( !o ) + return acc; + + if ( notSelfOrWrapped(o.reduce) ) + return o.reduce.apply(o, slice.call(arguments,1)); + + cxt = cxt || o; + for ( var name in o ) + acc = fn.call(cxt, acc, o[name], name, o); + + return acc; +} + +function attr(o, key, value, def){ + if ( o && notSelfOrWrapped(o.attr) ) + return o.attr.apply(o, slice.call(arguments,1)); + + if ( value !== undefined || def !== undefined ){ + o[key] = (value !== undefined ? value : def); + return o; + } else + return o[key]; +} + +function extend( A, B ){ + return slice.call(arguments,1).reduce(function(A, donor){ + return reduce(donor, function(o, v, k){ + return attr(o, k, v, o[k]); + }, A); + }, A); +} diff --git a/src/js/function.js b/src/js/function.js new file mode 100644 index 0000000..c52011f --- /dev/null +++ b/src/js/function.js @@ -0,0 +1,46 @@ +var WR_P = "__wraps__"; + + +function unwrap(fn){ + return ( fn && isFunction(fn) ) ? unwrap(fn[WR_P]) || fn : fn; +} + +Function.prototype.curry = curry; +function curry(){ + var fn = this + , args = Array.slice(arguments,0) + , L = unwrap(fn).length; + + function curried(){ + var _args = args.concat(Array.slice(arguments,0)); + if ( _args.length >= L ) + return fn.apply(this, _args); + else + return curry.apply(fn, _args); + } + curried[WR_P] = fn; + + return curried; +} + +Function.prototype.methodize = methodize; +function methodize() { + var fn = this; + if ( fn.__methodized__ ) + return fn.__methodized__; + + var m = fn.__methodized__ = + function(){ + return fn.apply(this, [this].concat(Array.slice(arguments, 0))); + }; + m[WR_P] = fn; + return m; +} + +/** Returns the declared name of a function. */ +Function.prototype.getName = getName; +function getName(){ + var fn = this; + return fn.className || fn.name || (fn+'').match( /function\s*([^\(]*)\(/ )[1] || ''; +} + diff --git a/src/js/js.js.php b/src/js/js.js.php new file mode 100644 index 0000000..8868e17 --- /dev/null +++ b/src/js/js.js.php @@ -0,0 +1,46 @@ + 0) { + $f = fopen($path, "r"); + echo fread($f, $size); + fclose($f); + } + if ($add_newline) echo "\n"; +} + +$jsjs_files = array( + 'type', + 'core', + 'function', + 'object', + 'array', + 'string', + 'number' +); + +function jsjs_list($path='') { + global $jsjs_files; + $path = $path ? $path : dirname($_SERVER["REQUEST_URI"]); + // echo $path; + foreach ($jsjs_files as $f) { + echo "\n"; + } +} + +function jsjs_dump($expose=false) { + global $jsjs_files; + if (!$expose) + dump_file("./_intro.js"); + foreach ($jsjs_files as $f) + dump_file("./$f.js"); + if (!$expose) + dump_file("./_outro.js"); +} + +if ( basename($_SERVER["SCRIPT_FILENAME"]) == basename(__FILE__) ) { + if ( $_REQUEST["list"] ) + jsjs_list(); + else + jsjs_dump(); +} \ No newline at end of file diff --git a/src/js/number.js b/src/js/number.js new file mode 100644 index 0000000..e69de29 diff --git a/src/js/object.js b/src/js/object.js new file mode 100644 index 0000000..e69de29 diff --git a/src/js/regexp.js b/src/js/regexp.js new file mode 100644 index 0000000..e69de29 diff --git a/src/js/string.js b/src/js/string.js new file mode 100644 index 0000000..e69de29 diff --git a/src/js/type.js b/src/js/type.js new file mode 100644 index 0000000..1fd1447 --- /dev/null +++ b/src/js/type.js @@ -0,0 +1,49 @@ +// Type Utilities // +// Much borrowed from jQuery + +var class2type = "Boolean Number String Function Array Date RegExp Object" + .split(" ") + .reduce(function(class2type, name) { + class2type[ "[object "+name+"]" ] = name.toLowerCase(); + return class2type; + }, {}); + +function type_of(obj){ + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; +} + +function isFunction(obj) { return type_of(obj) === "function"; } +function isString(obj) { return type_of(obj) === "string"; } +function isNumber(obj) { return type_of(obj) === "number"; } + +// A crude way of determining if an object is a window +function isWindow( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; +} + +function isPlainObject( 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 || type_of(obj) !== "object" || obj.nodeType || 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 ); +} + + diff --git a/src/lessly/bitgrid.js b/src/lessly/bitgrid.js new file mode 100644 index 0000000..37efb7b --- /dev/null +++ b/src/lessly/bitgrid.js @@ -0,0 +1,381 @@ +(function(){ + +// Constants +var CELL_X = 16 // 16x2 cells +, CELL_Y = 2 +, CELL_MASK = 0xffffffff +, A = [] // Empty array for guarded lookups +; + +/*@inline*/ +function toBits(x,y){ return (1 << (x % CELL_X)) << ((y % CELL_Y) * CELL_X); } +/*@inline*/ +function toX(x){ return Math.floor(x/CELL_X); } +/*@inline*/ +function toY(y){ return Math.floor(y/CELL_Y); } + + +/** + * A bitset grid (for path-mapping). + * TODO: Make rect() routines agnostic to cell dimensions + * TODO: Optimize set operations + */ +BitGrid = new Y.Class('BitGrid', { + init : function(w, h){ + this.width = w; + this.height = h; + + var x_cells = this.x_cells = toX(w) + , y_cells = this.y_cells = toY(h) + , cells = this.cells = [] + ; + + while (x_cells-- > 0) cells.push([]); + + // for (var x=0; x new acc + * Returns the final accumulated value. + */ + reduce : function(iter, acc, context){ + context = context || this; + var cells = this.cells + , xi_max = toX(this.width) + , yi_max = toY(this.height) + ; + for (var xi=0; xi<=xi_max; xi++) { + var col = cells[xi]; + for (var yi=0; yi new value + */ + map : function(iter, context){ + context = context || this; + var col, cell + , cells = this.cells + , xi_max = toX(this.width) + , yi_max = toY(this.height) + ; + for (var xi=0; xi<=xi_max; xi++) { + col = cells[xi]; + for (var yi=0; yi void + */ + each : function(iter, context){ + context = context || this; + var col + , cells = this.cells + , xi_max = toX(this.width) + , yi_max = toY(this.height) + ; + for (var xi=0; xi<=xi_max; xi++) { + col = cells[xi] || A; + for (var yi=0; yi this.width || y < 0 || y > this.height) + // return undefined; + this._getCol(x)[ toY(y) ] |= toBits(x,y); + return this; + }, + + addRect : function(x1,y1, x2,y2){ + var cells = this.cells + , x_min = toX(x1), x_max = toX(x2) + , y_min = toY(y1), y_max = toY(y2) + , y_min_ragged = ((y1%CELL_Y) !== 0) + , y_max_ragged = ((y2%CELL_Y) !== 1) + ; + + for (var xi=x_min; xi<=x_max; xi++) { + var xbits = 0xffff; + if ( xi === x_min ) { + var off = (x1%CELL_X); + xbits &= (0xffff >> off) << off; + } + if ( xi === x_max ) { + xbits &= 0xffff >> (CELL_X - (x2%CELL_X)); + } + var bits = (xbits << CELL_X) | xbits + , xcell = cells[xi] || (cells[xi] = []); + for (var yi=y_min; yi<=y_max; yi++) { + if (yi === y_min && y_min_ragged) { + xcell[yi] |= xbits << CELL_X; + } else if (yi === y_max && y_max_ragged) { + xcell[yi] |= xbits; + } else { + xcell[yi] |= bits; + } + } + } + return this; + }, + + addGrid : function(other){ + var cells = this.cells + , o_cells = other.cells + , x_len = o_cells.length + ; + for (var xi=0; xi> off) << off; + } + if ( xi === x_max ) { + xbits &= 0xffff >> (CELL_X - (x2%CELL_X)); + } + var bits = CELL_MASK ^ ((xbits << CELL_X) | xbits) + , xcell = cells[xi] || (cells[xi] = []); + for (var yi=y_min; yi<=y_max; yi++) { + if (yi === y_min && y_min_ragged) { + xcell[yi] &= CELL_MASK ^ (xbits << CELL_X); + } else if (yi === y_max && y_max_ragged) { + xcell[yi] &= CELL_MASK ^ xbits; + } else { + xcell[yi] &= bits; + } + } + } + return this; + }, + + subtractGrid : function(other){ + var cells = this.cells + , o_cells = other.cells + , x_len = o_cells.length + ; + for (var xi=0; xi