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