1 /** 2 * KineticJS JavaScript Library v3.9.4 3 * http://www.kineticjs.com/ 4 * Copyright 2012, Eric Rowell 5 * Licensed under the MIT or GPL Version 2 licenses. 6 * Date: Apr 28 2012 7 * 8 * Copyright (C) 2011 - 2012 by Eric Rowell 9 * 10 * Permission is hereby granted, free of charge, to any person obtaining a copy 11 * of this software and associated documentation files (the "Software"), to deal 12 * in the Software without restriction, including without limitation the rights 13 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 * copies of the Software, and to permit persons to whom the Software is 15 * furnished to do so, subject to the following conditions: 16 * 17 * The above copyright notice and this permission notice shall be included in 18 * all copies or substantial portions of the Software. 19 * 20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 * THE SOFTWARE. 27 */ 28 29 /////////////////////////////////////////////////////////////////////// 30 // Global Object 31 /////////////////////////////////////////////////////////////////////// 32 /** 33 * Kinetic Namespace 34 * @namespace 35 */ 36 var Kinetic = {}; 37 /** 38 * Kinetic Global Object 39 * @property {Object} GlobalObjet 40 */ 41 Kinetic.GlobalObject = { 42 stages: [], 43 idCounter: 0, 44 tempNodes: [], 45 animations: [], 46 animIdCounter: 0, 47 dragTimeInterval: 0, 48 maxDragTimeInterval: 20, 49 frame: { 50 time: 0, 51 timeDiff: 0, 52 lastTime: 0 53 }, 54 drag: { 55 moving: false, 56 node: undefined, 57 offset: { 58 x: 0, 59 y: 0 60 }, 61 lastDrawTime: 0 62 }, 63 extend: function(obj1, obj2) { 64 for(var key in obj2.prototype) { 65 if(obj2.prototype.hasOwnProperty(key) && obj1.prototype[key] === undefined) { 66 obj1.prototype[key] = obj2.prototype[key]; 67 } 68 } 69 }, 70 _pullNodes: function(stage) { 71 var tempNodes = this.tempNodes; 72 for(var n = 0; n < tempNodes.length; n++) { 73 var node = tempNodes[n]; 74 if(node.getStage() !== undefined && node.getStage()._id === stage._id) { 75 stage._addId(node); 76 stage._addName(node); 77 this.tempNodes.splice(n, 1); 78 n -= 1; 79 } 80 } 81 }, 82 /* 83 * animation support 84 */ 85 _addAnimation: function(anim) { 86 anim.id = this.animIdCounter++; 87 this.animations.push(anim); 88 }, 89 _removeAnimation: function(anim) { 90 var id = anim.id; 91 var animations = this.animations; 92 for(var n = 0; n < animations.length; n++) { 93 if(animations[n].id === id) { 94 this.animations.splice(n, 1); 95 return false; 96 } 97 } 98 }, 99 _runFrames: function() { 100 var nodes = {}; 101 for(var n = 0; n < this.animations.length; n++) { 102 var anim = this.animations[n]; 103 if(anim.node && anim.node._id !== undefined) { 104 nodes[anim.node._id] = anim.node; 105 } 106 anim.func(this.frame); 107 } 108 109 for(var key in nodes) { 110 nodes[key].draw(); 111 } 112 }, 113 _updateFrameObject: function() { 114 var date = new Date(); 115 var time = date.getTime(); 116 if(this.frame.lastTime === 0) { 117 this.frame.lastTime = time; 118 } 119 else { 120 this.frame.timeDiff = time - this.frame.lastTime; 121 this.frame.lastTime = time; 122 this.frame.time += this.frame.timeDiff; 123 } 124 }, 125 _animationLoop: function() { 126 if(this.animations.length > 0) { 127 this._updateFrameObject(); 128 this._runFrames(); 129 var that = this; 130 requestAnimFrame(function() { 131 that._animationLoop(); 132 }); 133 } 134 else { 135 this.frame.lastTime = 0; 136 } 137 }, 138 _handleAnimation: function() { 139 var that = this; 140 if(this.animations.length > 0) { 141 that._animationLoop(); 142 } 143 else { 144 this.frame.lastTime = 0; 145 } 146 }, 147 /* 148 * utilities 149 */ 150 _isElement: function(obj) { 151 return !!(obj && obj.nodeType == 1); 152 }, 153 _isFunction: function(obj) { 154 return !!(obj && obj.constructor && obj.call && obj.apply); 155 }, 156 _getPoint: function(arg) { 157 158 if(arg.length === 1) { 159 return arg[0]; 160 } 161 else { 162 return { 163 x: arg[0], 164 y: arg[1] 165 } 166 } 167 } 168 }; 169 170 window.requestAnimFrame = (function(callback) { 171 return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || 172 function(callback) { 173 window.setTimeout(callback, 1000 / 60); 174 }; 175 })(); 176 177 /////////////////////////////////////////////////////////////////////// 178 // Node 179 /////////////////////////////////////////////////////////////////////// 180 /** 181 * Node constructor. Nodes are entities that can move around 182 * and have events bound to them. They are the building blocks of a KineticJS 183 * application 184 * @constructor 185 * @param {Object} config 186 */ 187 Kinetic.Node = function(config) { 188 this.setDefaultAttrs({ 189 visible: true, 190 listening: true, 191 name: undefined, 192 alpha: 1, 193 x: 0, 194 y: 0, 195 scale: { 196 x: 1, 197 y: 1 198 }, 199 rotation: 0, 200 centerOffset: { 201 x: 0, 202 y: 0 203 }, 204 dragConstraint: 'none', 205 dragBounds: {}, 206 draggable: false 207 }); 208 209 this.eventListeners = {}; 210 this.setAttrs(config); 211 }; 212 /* 213 * Node methods 214 */ 215 Kinetic.Node.prototype = { 216 /** 217 * bind events to the node. KineticJS supports mouseover, mousemove, 218 * mouseout, mousedown, mouseup, click, dblclick, touchstart, touchmove, 219 * touchend, dbltap, dragstart, dragmove, and dragend. Pass in a string 220 * of event types delimmited by a space to bind multiple events at once 221 * such as 'mousedown mouseup mousemove'. include a namespace to bind an 222 * event by name such as 'click.foobar'. 223 * @param {String} typesStr 224 * @param {function} handler 225 */ 226 on: function(typesStr, handler) { 227 var types = typesStr.split(' '); 228 /* 229 * loop through types and attach event listeners to 230 * each one. eg. 'click mouseover.namespace mouseout' 231 * will create three event bindings 232 */ 233 for(var n = 0; n < types.length; n++) { 234 var type = types[n]; 235 var event = (type.indexOf('touch') === -1) ? 'on' + type : type; 236 var parts = event.split('.'); 237 var baseEvent = parts[0]; 238 var name = parts.length > 1 ? parts[1] : ''; 239 240 if(!this.eventListeners[baseEvent]) { 241 this.eventListeners[baseEvent] = []; 242 } 243 244 this.eventListeners[baseEvent].push({ 245 name: name, 246 handler: handler 247 }); 248 } 249 }, 250 /** 251 * remove event bindings from the node. Pass in a string of 252 * event types delimmited by a space to remove multiple event 253 * bindings at once such as 'mousedown mouseup mousemove'. 254 * include a namespace to remove an event binding by name 255 * such as 'click.foobar'. 256 * @param {String} typesStr 257 */ 258 off: function(typesStr) { 259 var types = typesStr.split(' '); 260 261 for(var n = 0; n < types.length; n++) { 262 var type = types[n]; 263 var event = (type.indexOf('touch') === -1) ? 'on' + type : type; 264 var parts = event.split('.'); 265 var baseEvent = parts[0]; 266 267 if(this.eventListeners[baseEvent] && parts.length > 1) { 268 var name = parts[1]; 269 270 for(var i = 0; i < this.eventListeners[baseEvent].length; i++) { 271 if(this.eventListeners[baseEvent][i].name === name) { 272 this.eventListeners[baseEvent].splice(i, 1); 273 if(this.eventListeners[baseEvent].length === 0) { 274 this.eventListeners[baseEvent] = undefined; 275 } 276 break; 277 } 278 } 279 } 280 else { 281 this.eventListeners[baseEvent] = undefined; 282 } 283 } 284 }, 285 /** 286 * get attrs 287 */ 288 getAttrs: function() { 289 return this.attrs; 290 }, 291 /** 292 * set default attrs 293 * @param {Object} confic 294 */ 295 setDefaultAttrs: function(config) { 296 // create attrs object if undefined 297 if(this.attrs === undefined) { 298 this.attrs = {}; 299 } 300 301 if(config) { 302 for(var key in config) { 303 var val = config[key]; 304 this.attrs[key] = config[key]; 305 } 306 } 307 }, 308 /** 309 * set attrs 310 * @param {Object} config 311 */ 312 setAttrs: function(config) { 313 var go = Kinetic.GlobalObject; 314 // set properties from config 315 if(config) { 316 for(var key in config) { 317 var val = config[key]; 318 319 /* 320 * add functions, DOM elements, and images 321 * directly to the node 322 */ 323 if(go._isFunction(val) || go._isElement(val)) { 324 this[key] = val; 325 } 326 /* 327 * add all other object types to attrs object 328 */ 329 else { 330 // handle special keys 331 switch (key) { 332 /* 333 * config properties that require a method to 334 * be set 335 */ 336 case 'draggable': 337 this.draggable(config[key]); 338 break; 339 case 'listening': 340 this.listen(config[key]); 341 break; 342 case 'rotationDeg': 343 this.attrs.rotation = config[key] * Math.PI / 180; 344 break; 345 /* 346 * config objects 347 */ 348 case 'centerOffset': 349 if(val.x !== undefined) { 350 this.attrs[key].x = val.x; 351 } 352 if(val.y !== undefined) { 353 this.attrs[key].y = val.y; 354 } 355 break; 356 case 'scale': 357 if(val.x !== undefined) { 358 this.attrs[key].x = val.x; 359 } 360 if(val.y !== undefined) { 361 this.attrs[key].y = val.y; 362 } 363 break; 364 case 'crop': 365 if(val.x !== undefined) { 366 this.attrs[key].x = val.x; 367 } 368 if(val.y !== undefined) { 369 this.attrs[key].y = val.y; 370 } 371 if(val.width !== undefined) { 372 this.attrs[key].width = val.width; 373 } 374 if(val.height !== undefined) { 375 this.attrs[key].height = val.height; 376 } 377 break; 378 default: 379 this.attrs[key] = config[key]; 380 break; 381 } 382 } 383 } 384 } 385 }, 386 /** 387 * determine if shape is visible or not 388 */ 389 isVisible: function() { 390 return this.attrs.visible; 391 }, 392 /** 393 * show node 394 */ 395 show: function() { 396 this.attrs.visible = true; 397 }, 398 /** 399 * hide node 400 */ 401 hide: function() { 402 this.attrs.visible = false; 403 }, 404 /** 405 * get zIndex 406 */ 407 getZIndex: function() { 408 return this.index; 409 }, 410 /** 411 * get absolute z-index by taking into account 412 * all parent and sibling indices 413 */ 414 getAbsoluteZIndex: function() { 415 var level = this.getLevel(); 416 var stage = this.getStage(); 417 var that = this; 418 var index = 0; 419 function addChildren(children) { 420 var nodes = []; 421 for(var n = 0; n < children.length; n++) { 422 var child = children[n]; 423 index++; 424 425 if(child.nodeType !== 'Shape') { 426 nodes = nodes.concat(child.getChildren()); 427 } 428 429 if(child._id === that._id) { 430 n = children.length; 431 } 432 } 433 434 if(nodes.length > 0 && nodes[0].getLevel() <= level) { 435 addChildren(nodes); 436 } 437 } 438 if(that.nodeType !== 'Stage') { 439 addChildren(that.getStage().getChildren()); 440 } 441 442 return index; 443 }, 444 /** 445 * get node level in node tree 446 */ 447 getLevel: function() { 448 var level = 0; 449 var parent = this.parent; 450 while(parent) { 451 level++; 452 parent = parent.parent; 453 } 454 return level; 455 }, 456 /** 457 * set node scale. If only one parameter is passed in, 458 * then both scaleX and scaleY are set with that parameter 459 * @param {Number} scaleX 460 * @param {Number} scaleY 461 */ 462 setScale: function(scaleX, scaleY) { 463 if(scaleY) { 464 this.attrs.scale.x = scaleX; 465 this.attrs.scale.y = scaleY; 466 } 467 else { 468 this.attrs.scale.x = scaleX; 469 this.attrs.scale.y = scaleX; 470 } 471 }, 472 /** 473 * get scale 474 */ 475 getScale: function() { 476 return this.attrs.scale; 477 }, 478 /** 479 * set node position 480 * @param {Object} point 481 */ 482 setPosition: function() { 483 var pos = Kinetic.GlobalObject._getPoint(arguments); 484 this.attrs.x = pos.x; 485 this.attrs.y = pos.y; 486 }, 487 /** 488 * set node x position 489 * @param {Number} x 490 */ 491 setX: function(x) { 492 this.attrs.x = x; 493 }, 494 /** 495 * set node y position 496 * @param {Number} y 497 */ 498 setY: function(y) { 499 this.attrs.y = y; 500 }, 501 /** 502 * get node x position 503 */ 504 getX: function() { 505 return this.attrs.x; 506 }, 507 /** 508 * get node y position 509 */ 510 getY: function() { 511 return this.attrs.y; 512 }, 513 /** 514 * set detection type 515 * @param {String} type can be "path" or "pixel" 516 */ 517 setDetectionType: function(type) { 518 this.attrs.detectionType = type; 519 }, 520 /** 521 * get detection type 522 */ 523 getDetectionType: function() { 524 return this.attrs.detectionType; 525 }, 526 /** 527 * get node position relative to container 528 */ 529 getPosition: function() { 530 return { 531 x: this.attrs.x, 532 y: this.attrs.y 533 }; 534 }, 535 /** 536 * get absolute position relative to stage 537 */ 538 getAbsolutePosition: function() { 539 return this.getAbsoluteTransform().getTranslation(); 540 }, 541 /** 542 * set absolute position relative to stage 543 * @param {Object} pos object containing an x and 544 * y property 545 */ 546 setAbsolutePosition: function() { 547 var pos = Kinetic.GlobalObject._getPoint(arguments); 548 /* 549 * save rotation and scale and 550 * then remove them from the transform 551 */ 552 var rot = this.attrs.rotation; 553 var scale = { 554 x: this.attrs.scale.x, 555 y: this.attrs.scale.y 556 }; 557 var centerOffset = { 558 x: this.attrs.centerOffset.x, 559 y: this.attrs.centerOffset.y 560 }; 561 562 this.attrs.rotation = 0; 563 this.attrs.scale = { 564 x: 1, 565 y: 1 566 }; 567 568 /* 569 this.attrs.centerOffset = { 570 x: 0, 571 y: 0 572 }; 573 */ 574 575 //this.move(-1 * this.attrs.centerOffset.x, -1 * this.attrs.centerOffset.y); 576 577 // unravel transform 578 var it = this.getAbsoluteTransform(); 579 it.invert(); 580 it.translate(pos.x, pos.y); 581 pos = { 582 x: this.attrs.x + it.getTranslation().x, 583 y: this.attrs.y + it.getTranslation().y 584 }; 585 586 this.setPosition(pos.x, pos.y); 587 588 //this.move(-1* this.attrs.centerOffset.x, -1* this.attrs.centerOffset.y); 589 590 // restore rotation and scale 591 this.rotate(rot); 592 this.attrs.scale = { 593 x: scale.x, 594 y: scale.y 595 }; 596 597 }, 598 /** 599 * move node by an amount 600 * @param {Number} x 601 * @param {Number} y 602 */ 603 move: function(x, y) { 604 this.attrs.x += x; 605 this.attrs.y += y; 606 }, 607 /** 608 * set node rotation in radians 609 * @param {Number} theta 610 */ 611 setRotation: function(theta) { 612 this.attrs.rotation = theta; 613 }, 614 /** 615 * set node rotation in degrees 616 * @param {Number} deg 617 */ 618 setRotationDeg: function(deg) { 619 this.attrs.rotation = (deg * Math.PI / 180); 620 }, 621 /** 622 * get rotation in radians 623 */ 624 getRotation: function() { 625 return this.attrs.rotation; 626 }, 627 /** 628 * get rotation in degrees 629 */ 630 getRotationDeg: function() { 631 return this.attrs.rotation * 180 / Math.PI; 632 }, 633 /** 634 * rotate node by an amount in radians 635 * @param {Number} theta 636 */ 637 rotate: function(theta) { 638 this.attrs.rotation += theta; 639 }, 640 /** 641 * rotate node by an amount in degrees 642 * @param {Number} deg 643 */ 644 rotateDeg: function(deg) { 645 this.attrs.rotation += (deg * Math.PI / 180); 646 }, 647 /** 648 * listen or don't listen to events 649 * @param {Boolean} listening 650 */ 651 listen: function(listening) { 652 this.attrs.listening = listening; 653 }, 654 /** 655 * move node to top 656 */ 657 moveToTop: function() { 658 var index = this.index; 659 this.parent.children.splice(index, 1); 660 this.parent.children.push(this); 661 this.parent._setChildrenIndices(); 662 }, 663 /** 664 * move node up 665 */ 666 moveUp: function() { 667 var index = this.index; 668 this.parent.children.splice(index, 1); 669 this.parent.children.splice(index + 1, 0, this); 670 this.parent._setChildrenIndices(); 671 }, 672 /** 673 * move node down 674 */ 675 moveDown: function() { 676 var index = this.index; 677 if(index > 0) { 678 this.parent.children.splice(index, 1); 679 this.parent.children.splice(index - 1, 0, this); 680 this.parent._setChildrenIndices(); 681 } 682 }, 683 /** 684 * move node to bottom 685 */ 686 moveToBottom: function() { 687 var index = this.index; 688 this.parent.children.splice(index, 1); 689 this.parent.children.unshift(this); 690 this.parent._setChildrenIndices(); 691 }, 692 /** 693 * set zIndex 694 * @param {int} zIndex 695 */ 696 setZIndex: function(zIndex) { 697 var index = this.index; 698 this.parent.children.splice(index, 1); 699 this.parent.children.splice(zIndex, 0, this); 700 this.parent._setChildrenIndices(); 701 }, 702 /** 703 * set alpha. Alpha values range from 0 to 1. 704 * A node with an alpha of 0 is fully transparent, and a node 705 * with an alpha of 1 is fully opaque 706 * @param {Object} alpha 707 */ 708 setAlpha: function(alpha) { 709 this.attrs.alpha = alpha; 710 }, 711 /** 712 * get alpha. Alpha values range from 0 to 1. 713 * A node with an alpha of 0 is fully transparent, and a node 714 * with an alpha of 1 is fully opaque 715 */ 716 getAlpha: function() { 717 return this.attrs.alpha; 718 }, 719 /** 720 * get absolute alpha 721 */ 722 getAbsoluteAlpha: function() { 723 var absAlpha = 1; 724 var node = this; 725 // traverse upwards 726 while(node.nodeType !== 'Stage') { 727 absAlpha *= node.attrs.alpha; 728 node = node.parent; 729 } 730 return absAlpha; 731 }, 732 /** 733 * enable or disable drag and drop 734 * @param {Boolean} isDraggable 735 */ 736 draggable: function(isDraggable) { 737 if(this.attrs.draggable !== isDraggable) { 738 if(isDraggable) { 739 this._initDrag(); 740 } 741 else { 742 this._dragCleanup(); 743 } 744 this.attrs.draggable = isDraggable; 745 } 746 }, 747 /** 748 * determine if node is currently in drag and drop mode 749 */ 750 isDragging: function() { 751 var go = Kinetic.GlobalObject; 752 return go.drag.node !== undefined && go.drag.node._id === this._id && go.drag.moving; 753 }, 754 /** 755 * move node to another container 756 * @param {Container} newContainer 757 */ 758 moveTo: function(newContainer) { 759 var parent = this.parent; 760 // remove from parent's children 761 parent.children.splice(this.index, 1); 762 parent._setChildrenIndices(); 763 764 // add to new parent 765 newContainer.children.push(this); 766 this.index = newContainer.children.length - 1; 767 this.parent = newContainer; 768 newContainer._setChildrenIndices(); 769 }, 770 /** 771 * get parent container 772 */ 773 getParent: function() { 774 return this.parent; 775 }, 776 /** 777 * get layer associated to node 778 */ 779 getLayer: function() { 780 if(this.nodeType === 'Layer') { 781 return this; 782 } 783 else { 784 return this.getParent().getLayer(); 785 } 786 }, 787 /** 788 * get stage associated to node 789 */ 790 getStage: function() { 791 if(this.nodeType === 'Stage') { 792 return this; 793 } 794 else { 795 if(this.getParent() === undefined) { 796 return undefined; 797 } 798 else { 799 return this.getParent().getStage(); 800 } 801 } 802 }, 803 /** 804 * get name 805 */ 806 getName: function() { 807 return this.attrs.name; 808 }, 809 /** 810 * set center offset 811 * @param {Number} x 812 * @param {Number} y 813 */ 814 setCenterOffset: function(x, y) { 815 this.attrs.centerOffset.x = x; 816 this.attrs.centerOffset.y = y; 817 }, 818 /** 819 * get center offset 820 */ 821 getCenterOffset: function() { 822 return this.attrs.centerOffset; 823 }, 824 /** 825 * transition node to another state. Any property that can accept a real 826 * number can be transitioned, including x, y, rotation, alpha, strokeWidth, 827 * radius, scale.x, scale.y, centerOffset.x, centerOffset.y, etc. 828 * @param {Object} config 829 * @config {Number} [duration] duration that the transition runs in seconds 830 * @config {String} [easing] easing function. can be linear, ease-in, ease-out, ease-in-out, 831 * back-ease-in, back-ease-out, back-ease-in-out, elastic-ease-in, elastic-ease-out, 832 * elastic-ease-in-out, bounce-ease-out, bounce-ease-in, bounce-ease-in-out, 833 * strong-ease-in, strong-ease-out, or strong-ease-in-out 834 * linear is the default 835 * @config {Function} [callback] callback function to be executed when 836 * transition completes 837 */ 838 transitionTo: function(config) { 839 var go = Kinetic.GlobalObject; 840 841 /* 842 * clear transition if one is currently running for this 843 * node 844 */ 845 if(this.transAnim !== undefined) { 846 go._removeAnimation(this.transAnim); 847 this.transAnim = undefined; 848 } 849 850 /* 851 * create new transition 852 */ 853 var node = this.nodeType === 'Stage' ? this : this.getLayer(); 854 var that = this; 855 var trans = new Kinetic.Transition(this, config); 856 var anim = { 857 func: function() { 858 trans.onEnterFrame(); 859 }, 860 node: node 861 }; 862 863 // store reference to transition animation 864 this.transAnim = anim; 865 866 /* 867 * adding the animation with the addAnimation 868 * method auto generates an id 869 */ 870 go._addAnimation(anim); 871 872 // subscribe to onFinished for first tween 873 trans.onFinished = function() { 874 // remove animation 875 go._removeAnimation(anim); 876 that.transAnim = undefined; 877 878 // callback 879 if(config.callback !== undefined) { 880 config.callback(); 881 } 882 883 anim.node.draw(); 884 }; 885 // auto start 886 trans.start(); 887 888 go._handleAnimation(); 889 890 return trans; 891 }, 892 /** 893 * set drag constraint 894 * @param {String} constraint 895 */ 896 setDragConstraint: function(constraint) { 897 this.attrs.dragConstraint = constraint; 898 }, 899 /** 900 * get drag constraint 901 */ 902 getDragConstraint: function() { 903 return this.attrs.dragConstraint; 904 }, 905 /** 906 * set drag bounds 907 * @param {Object} bounds 908 * @config {Number} [left] left bounds position 909 * @config {Number} [top] top bounds position 910 * @config {Number} [right] right bounds position 911 * @config {Number} [bottom] bottom bounds position 912 */ 913 setDragBounds: function(bounds) { 914 this.attrs.dragBounds = bounds; 915 }, 916 /** 917 * get drag bounds 918 */ 919 getDragBounds: function() { 920 return this.attrs.dragBounds; 921 }, 922 /** 923 * get transform of the node while taking into 924 * account the transforms of its parents 925 */ 926 getAbsoluteTransform: function() { 927 // absolute transform 928 var am = new Kinetic.Transform(); 929 930 var family = []; 931 var parent = this.parent; 932 933 family.unshift(this); 934 while(parent) { 935 family.unshift(parent); 936 parent = parent.parent; 937 } 938 939 for(var n = 0; n < family.length; n++) { 940 var node = family[n]; 941 var m = node.getTransform(); 942 943 am.multiply(m); 944 } 945 946 return am; 947 }, 948 /** 949 * get transform of the node while not taking 950 * into account the transforms of its parents 951 */ 952 getTransform: function() { 953 var m = new Kinetic.Transform(); 954 955 if(this.attrs.x !== 0 || this.attrs.y !== 0) { 956 m.translate(this.attrs.x, this.attrs.y); 957 } 958 if(this.attrs.rotation !== 0) { 959 m.rotate(this.attrs.rotation); 960 } 961 if(this.attrs.scale.x !== 1 || this.attrs.scale.y !== 1) { 962 m.scale(this.attrs.scale.x, this.attrs.scale.y); 963 } 964 965 return m; 966 }, 967 /** 968 * initialize drag and drop 969 */ 970 _initDrag: function() { 971 this._dragCleanup(); 972 var go = Kinetic.GlobalObject; 973 var that = this; 974 this.on('mousedown.initdrag touchstart.initdrag', function(evt) { 975 var stage = that.getStage(); 976 var pos = stage.getUserPosition(); 977 978 if(pos) { 979 var m = that.getTransform().getTranslation(); 980 var am = that.getAbsoluteTransform().getTranslation(); 981 go.drag.node = that; 982 go.drag.offset.x = pos.x - that.getAbsoluteTransform().getTranslation().x; 983 go.drag.offset.y = pos.y - that.getAbsoluteTransform().getTranslation().y; 984 } 985 }); 986 }, 987 /** 988 * remove drag and drop event listener 989 */ 990 _dragCleanup: function() { 991 this.off('mousedown.initdrag'); 992 this.off('touchstart.initdrag'); 993 }, 994 /** 995 * handle node events 996 * @param {String} eventType 997 * @param {Event} evt 998 */ 999 _handleEvents: function(eventType, evt) { 1000 if(this.nodeType === 'Shape') { 1001 evt.shape = this; 1002 } 1003 var stage = this.getStage(); 1004 this._handleEvent(this, stage.mouseoverShape, stage.mouseoutShape, eventType, evt); 1005 }, 1006 /** 1007 * handle node event 1008 */ 1009 _handleEvent: function(node, mouseoverNode, mouseoutNode, eventType, evt) { 1010 var el = node.eventListeners; 1011 var okayToRun = true; 1012 1013 /* 1014 * determine if event handler should be skipped by comparing 1015 * parent nodes 1016 */ 1017 if(eventType === 'onmouseover' && mouseoutNode && mouseoutNode._id === node._id) { 1018 okayToRun = false; 1019 } 1020 else if(eventType === 'onmouseout' && mouseoverNode && mouseoverNode._id === node._id) { 1021 okayToRun = false; 1022 } 1023 1024 if(el[eventType] && okayToRun) { 1025 var events = el[eventType]; 1026 for(var i = 0; i < events.length; i++) { 1027 events[i].handler.apply(node, [evt]); 1028 } 1029 } 1030 1031 var mouseoverParent = mouseoverNode ? mouseoverNode.parent : undefined; 1032 var mouseoutParent = mouseoutNode ? mouseoutNode.parent : undefined; 1033 1034 // simulate event bubbling 1035 if(!evt.cancelBubble && node.parent.nodeType !== 'Stage') { 1036 this._handleEvent(node.parent, mouseoverParent, mouseoutParent, eventType, evt); 1037 } 1038 } 1039 }; 1040 1041 /////////////////////////////////////////////////////////////////////// 1042 // Container 1043 /////////////////////////////////////////////////////////////////////// 1044 1045 /** 1046 * Container constructor. Containers are used to contain nodes or other containers 1047 * @constructor 1048 */ 1049 Kinetic.Container = function() { 1050 this.children = []; 1051 }; 1052 /* 1053 * Container methods 1054 */ 1055 Kinetic.Container.prototype = { 1056 /** 1057 * get children 1058 */ 1059 getChildren: function() { 1060 return this.children; 1061 }, 1062 /** 1063 * remove all children 1064 */ 1065 removeChildren: function() { 1066 while(this.children.length > 0) { 1067 this.remove(this.children[0]); 1068 } 1069 }, 1070 /** 1071 * remove child from container 1072 * @param {Node} child 1073 */ 1074 _remove: function(child) { 1075 if(child.index !== undefined && this.children[child.index]._id == child._id) { 1076 var stage = this.getStage(); 1077 if(stage !== undefined) { 1078 stage._removeId(child); 1079 stage._removeName(child); 1080 } 1081 1082 var go = Kinetic.GlobalObject; 1083 for(var n = 0; n < go.tempNodes.length; n++) { 1084 var node = go.tempNodes[n]; 1085 if(node._id === child._id) { 1086 go.tempNodes.splice(n, 1); 1087 n = go.tempNodes.length; 1088 } 1089 } 1090 1091 this.children.splice(child.index, 1); 1092 this._setChildrenIndices(); 1093 child = undefined; 1094 } 1095 }, 1096 /** 1097 * return an array of nodes that match the selector. Use '#' for id selections 1098 * and '.' for name selections 1099 * ex: 1100 * var node = stage.get('#foo'); // selects node with id foo 1101 * var nodes = layer.get('.bar'); // selects nodes with name bar inside layer 1102 * @param {String} selector 1103 */ 1104 get: function(selector) { 1105 var stage = this.getStage(); 1106 var arr; 1107 var key = selector.slice(1); 1108 if(selector.charAt(0) === '#') { 1109 arr = stage.ids[key] !== undefined ? [stage.ids[key]] : []; 1110 } 1111 else if(selector.charAt(0) === '.') { 1112 arr = stage.names[key] !== undefined ? stage.names[key] : []; 1113 } 1114 else if(selector === 'Shape' || selector === 'Group' || selector === 'Layer') { 1115 return this._getNodes(selector); 1116 } 1117 else { 1118 return false; 1119 } 1120 1121 var retArr = []; 1122 for(var n = 0; n < arr.length; n++) { 1123 var node = arr[n]; 1124 if(this.isAncestorOf(node)) { 1125 retArr.push(node); 1126 } 1127 } 1128 1129 return retArr; 1130 }, 1131 /** 1132 * determine if node is an ancestor 1133 * of descendant 1134 * @param {Kinetic.Node} node 1135 */ 1136 isAncestorOf: function(node) { 1137 if(this.nodeType === 'Stage') { 1138 return true; 1139 } 1140 1141 var parent = node.getParent(); 1142 while(parent) { 1143 if(parent._id === this._id) { 1144 return true; 1145 } 1146 parent = parent.getParent(); 1147 } 1148 1149 return false; 1150 }, 1151 /** 1152 * get all shapes inside container 1153 */ 1154 _getNodes: function(sel) { 1155 var arr = []; 1156 function traverse(cont) { 1157 var children = cont.getChildren(); 1158 for(var n = 0; n < children.length; n++) { 1159 var child = children[n]; 1160 if(child.nodeType === sel) { 1161 arr.push(child); 1162 } 1163 else if(child.nodeType !== 'Shape') { 1164 traverse(child); 1165 } 1166 } 1167 } 1168 traverse(this); 1169 1170 return arr; 1171 }, 1172 /** 1173 * draw children 1174 */ 1175 _drawChildren: function() { 1176 var stage = this.getStage(); 1177 var children = this.children; 1178 for(var n = 0; n < children.length; n++) { 1179 var child = children[n]; 1180 if(child.nodeType === 'Shape' && child.isVisible() && stage.isVisible()) { 1181 child._draw(child.getLayer()); 1182 } 1183 else { 1184 child._draw(); 1185 } 1186 } 1187 }, 1188 /** 1189 * add node to container 1190 * @param {Node} child 1191 */ 1192 _add: function(child) { 1193 child._id = Kinetic.GlobalObject.idCounter++; 1194 child.index = this.children.length; 1195 child.parent = this; 1196 1197 this.children.push(child); 1198 1199 var stage = child.getStage(); 1200 if(stage === undefined) { 1201 var go = Kinetic.GlobalObject; 1202 go.tempNodes.push(child); 1203 } 1204 else { 1205 stage._addId(child); 1206 stage._addName(child); 1207 1208 /* 1209 * pull in other nodes that are now linked 1210 * to a stage 1211 */ 1212 var go = Kinetic.GlobalObject; 1213 go._pullNodes(stage); 1214 } 1215 }, 1216 /** 1217 * set children indices 1218 */ 1219 _setChildrenIndices: function() { 1220 /* 1221 * if reordering Layers, remove all canvas elements 1222 * from the container except the buffer and backstage canvases 1223 * and then readd all the layers 1224 */ 1225 if(this.nodeType === 'Stage') { 1226 var canvases = this.content.children; 1227 var bufferCanvas = canvases[0]; 1228 var backstageCanvas = canvases[1]; 1229 1230 this.content.innerHTML = ''; 1231 this.content.appendChild(bufferCanvas); 1232 this.content.appendChild(backstageCanvas); 1233 } 1234 1235 for(var n = 0; n < this.children.length; n++) { 1236 this.children[n].index = n; 1237 1238 if(this.nodeType === 'Stage') { 1239 this.content.appendChild(this.children[n].canvas); 1240 } 1241 } 1242 } 1243 }; 1244 1245 /////////////////////////////////////////////////////////////////////// 1246 // Stage 1247 /////////////////////////////////////////////////////////////////////// 1248 /** 1249 * Stage constructor. A stage is used to contain multiple layers and handle 1250 * animations 1251 * @constructor 1252 * @augments Kinetic.Container 1253 * @augments Kinetic.Node 1254 * @param {String|DomElement} cont Container id or DOM element 1255 * @param {int} width 1256 * @param {int} height 1257 */ 1258 Kinetic.Stage = function(config) { 1259 this.setDefaultAttrs({ 1260 width: 400, 1261 height: 200 1262 }); 1263 1264 this.nodeType = 'Stage'; 1265 1266 /* 1267 * if container is a string, assume it's an id for 1268 * a DOM element 1269 */ 1270 if( typeof config.container === 'string') { 1271 config.container = document.getElementById(config.container); 1272 } 1273 1274 // call super constructors 1275 Kinetic.Container.apply(this, []); 1276 Kinetic.Node.apply(this, [config]); 1277 1278 this.container = config.container; 1279 this.content = document.createElement('div'); 1280 this.dblClickWindow = 400; 1281 1282 this._setDefaults(); 1283 1284 // set stage id 1285 this._id = Kinetic.GlobalObject.idCounter++; 1286 1287 this._buildDOM(); 1288 this._listen(); 1289 this._prepareDrag(); 1290 1291 var go = Kinetic.GlobalObject; 1292 go.stages.push(this); 1293 this._addId(this); 1294 this._addName(this); 1295 }; 1296 /* 1297 * Stage methods 1298 */ 1299 Kinetic.Stage.prototype = { 1300 /** 1301 * sets onFrameFunc for animation 1302 * @param {function} func 1303 */ 1304 onFrame: function(func) { 1305 var go = Kinetic.GlobalObject; 1306 this.anim = { 1307 func: func 1308 }; 1309 }, 1310 /** 1311 * start animation 1312 */ 1313 start: function() { 1314 if(!this.animRunning) { 1315 var go = Kinetic.GlobalObject; 1316 go._addAnimation(this.anim); 1317 go._handleAnimation(); 1318 this.animRunning = true; 1319 } 1320 }, 1321 /** 1322 * stop animation 1323 */ 1324 stop: function() { 1325 var go = Kinetic.GlobalObject; 1326 go._removeAnimation(this.anim); 1327 this.animRunning = false; 1328 }, 1329 /** 1330 * draw children 1331 */ 1332 draw: function() { 1333 this._drawChildren(); 1334 }, 1335 /** 1336 * set stage size 1337 * @param {int} width 1338 * @param {int} height 1339 */ 1340 setSize: function(width, height) { 1341 // set stage dimensions 1342 this.attrs.width = width; 1343 this.attrs.height = height; 1344 1345 // set content dimensions 1346 this.content.style.width = this.attrs.width + 'px'; 1347 this.content.style.height = this.attrs.height + 'px'; 1348 1349 // set buffer layer and path layer sizes 1350 this.bufferLayer.getCanvas().width = width; 1351 this.bufferLayer.getCanvas().height = height; 1352 this.pathLayer.getCanvas().width = width; 1353 this.pathLayer.getCanvas().height = height; 1354 1355 // set user defined layer dimensions 1356 var layers = this.children; 1357 for(var n = 0; n < layers.length; n++) { 1358 var layer = layers[n]; 1359 layer.getCanvas().width = width; 1360 layer.getCanvas().height = height; 1361 layer.draw(); 1362 } 1363 }, 1364 /** 1365 * return stage size 1366 */ 1367 getSize: function() { 1368 return { 1369 width: this.attrs.width, 1370 height: this.attrs.height 1371 }; 1372 }, 1373 /** 1374 * clear all layers 1375 */ 1376 clear: function() { 1377 var layers = this.children; 1378 for(var n = 0; n < layers.length; n++) { 1379 layers[n].clear(); 1380 } 1381 }, 1382 /** 1383 * Creates a composite data URL and passes it to a callback. If MIME type is not 1384 * specified, then "image/png" will result. For "image/jpeg", specify a quality 1385 * level as quality (range 0.0 - 1.0) 1386 * @param {function} callback 1387 * @param {String} mimeType (optional) 1388 * @param {Number} quality (optional) 1389 */ 1390 toDataURL: function(callback, mimeType, quality) { 1391 var bufferLayer = this.bufferLayer; 1392 var bufferContext = bufferLayer.getContext(); 1393 var layers = this.children; 1394 var that = this; 1395 1396 function addLayer(n) { 1397 var dataURL = layers[n].getCanvas().toDataURL(); 1398 var imageObj = new Image(); 1399 imageObj.onload = function() { 1400 bufferContext.drawImage(this, 0, 0); 1401 n++; 1402 if(n < layers.length) { 1403 addLayer(n); 1404 } 1405 else { 1406 try { 1407 // If this call fails (due to browser bug, like in Firefox 3.6), 1408 // then revert to previous no-parameter image/png behavior 1409 callback(bufferLayer.getCanvas().toDataURL(mimeType, quality)); 1410 } 1411 catch(exception) { 1412 callback(bufferLayer.getCanvas().toDataURL()); 1413 } 1414 } 1415 }; 1416 imageObj.src = dataURL; 1417 } 1418 1419 bufferLayer.clear(); 1420 addLayer(0); 1421 }, 1422 /** 1423 * serialize stage and children as a JSON object 1424 */ 1425 toJSON: function() { 1426 var go = Kinetic.GlobalObject; 1427 1428 function addNode(node) { 1429 var obj = {}; 1430 obj.attrs = node.attrs; 1431 1432 obj.nodeType = node.nodeType; 1433 obj.shapeType = node.shapeType; 1434 1435 if(node.nodeType !== 'Shape') { 1436 obj.children = []; 1437 1438 var children = node.getChildren(); 1439 for(var n = 0; n < children.length; n++) { 1440 var child = children[n]; 1441 obj.children.push(addNode(child)); 1442 } 1443 } 1444 1445 return obj; 1446 } 1447 return JSON.stringify(addNode(this)); 1448 }, 1449 /** 1450 * reset stage to default state 1451 */ 1452 reset: function() { 1453 // remove children 1454 this.removeChildren(); 1455 1456 // reset stage defaults 1457 this._setDefaults(); 1458 1459 // reset node attrs 1460 this.setDefaultAttrs({ 1461 visible: true, 1462 listening: true, 1463 name: undefined, 1464 alpha: 1, 1465 x: 0, 1466 y: 0, 1467 scale: { 1468 x: 1, 1469 y: 1 1470 }, 1471 rotation: 0, 1472 centerOffset: { 1473 x: 0, 1474 y: 0 1475 }, 1476 dragConstraint: 'none', 1477 dragBounds: {}, 1478 draggable: false 1479 }); 1480 }, 1481 /** 1482 * load stage with JSON string. De-serializtion does not generate custom 1483 * shape drawing functions, images, or event handlers (this would make the 1484 * serialized object huge). If your app uses custom shapes, images, and 1485 * event handlers (it probably does), then you need to select the appropriate 1486 * shapes after loading the stage and set these properties via on(), setDrawFunc(), 1487 * and setImage() 1488 * @param {String} JSON string 1489 */ 1490 load: function(json) { 1491 this.reset(); 1492 1493 function loadNode(node, obj) { 1494 var children = obj.children; 1495 if(children !== undefined) { 1496 for(var n = 0; n < children.length; n++) { 1497 var child = children[n]; 1498 var type; 1499 1500 // determine type 1501 if(child.nodeType === 'Shape') { 1502 // add custom shape 1503 if(child.shapeType === undefined) { 1504 type = 'Shape'; 1505 } 1506 // add standard shape 1507 else { 1508 type = child.shapeType; 1509 } 1510 } 1511 else { 1512 type = child.nodeType; 1513 } 1514 1515 var no = new Kinetic[type](child.attrs); 1516 node.add(no); 1517 loadNode(no, child); 1518 } 1519 } 1520 } 1521 var obj = JSON.parse(json); 1522 1523 // copy over stage properties 1524 this.attrs = obj.attrs; 1525 1526 loadNode(this, obj); 1527 this.draw(); 1528 }, 1529 /** 1530 * remove layer from stage 1531 * @param {Layer} layer 1532 */ 1533 remove: function(layer) { 1534 /* 1535 * remove canvas DOM from the document if 1536 * it exists 1537 */ 1538 try { 1539 this.content.removeChild(layer.canvas); 1540 } 1541 catch(e) { 1542 } 1543 this._remove(layer); 1544 }, 1545 /** 1546 * add layer to stage 1547 * @param {Layer} layer 1548 */ 1549 add: function(layer) { 1550 layer.canvas.width = this.attrs.width; 1551 layer.canvas.height = this.attrs.height; 1552 this._add(layer); 1553 1554 // draw layer and append canvas to container 1555 layer.draw(); 1556 this.content.appendChild(layer.canvas); 1557 }, 1558 /** 1559 * get mouse position for desktop apps 1560 * @param {Event} evt 1561 */ 1562 getMousePosition: function(evt) { 1563 return this.mousePos; 1564 }, 1565 /** 1566 * get touch position for mobile apps 1567 * @param {Event} evt 1568 */ 1569 getTouchPosition: function(evt) { 1570 return this.touchPos; 1571 }, 1572 /** 1573 * get user position (mouse position or touch position) 1574 * @param {Event} evt 1575 */ 1576 getUserPosition: function(evt) { 1577 return this.getTouchPosition() || this.getMousePosition(); 1578 }, 1579 /** 1580 * get container DOM element 1581 */ 1582 getContainer: function() { 1583 return this.container; 1584 }, 1585 /** 1586 * get content DOM element 1587 */ 1588 getContent: function() { 1589 return this.content; 1590 }, 1591 /** 1592 * get stage 1593 */ 1594 getStage: function() { 1595 return this; 1596 }, 1597 /** 1598 * get width 1599 */ 1600 getWidth: function() { 1601 return this.attrs.width; 1602 }, 1603 /** 1604 * get height 1605 */ 1606 getHeight: function() { 1607 return this.attrs.height; 1608 }, 1609 /** 1610 * get shapes that intersect a point 1611 * @param {Object} point 1612 */ 1613 getIntersections: function() { 1614 var pos = Kinetic.GlobalObject._getPoint(arguments); 1615 var arr = []; 1616 var shapes = this.get('Shape'); 1617 1618 for(var n = 0; n < shapes.length; n++) { 1619 var shape = shapes[n]; 1620 if(shape.intersects(pos)) { 1621 arr.push(shape); 1622 } 1623 } 1624 1625 return arr; 1626 }, 1627 /** 1628 * get stage DOM node, which is a div element 1629 * with the class name "kineticjs-content" 1630 */ 1631 getDOM: function() { 1632 return this.content; 1633 }, 1634 /** 1635 * detect event 1636 * @param {Shape} shape 1637 */ 1638 _detectEvent: function(shape, evt) { 1639 var isDragging = Kinetic.GlobalObject.drag.moving; 1640 var go = Kinetic.GlobalObject; 1641 var pos = this.getUserPosition(); 1642 var el = shape.eventListeners; 1643 1644 if(this.targetShape && shape._id === this.targetShape._id) { 1645 this.targetFound = true; 1646 } 1647 1648 if(shape.attrs.visible && pos !== undefined && shape.intersects(pos)) { 1649 // handle onmousedown 1650 if(!isDragging && this.mouseDown) { 1651 this.mouseDown = false; 1652 this.clickStart = true; 1653 shape._handleEvents('onmousedown', evt); 1654 return true; 1655 } 1656 // handle onmouseup & onclick 1657 else if(this.mouseUp) { 1658 this.mouseUp = false; 1659 shape._handleEvents('onmouseup', evt); 1660 1661 // detect if click or double click occurred 1662 if(this.clickStart) { 1663 /* 1664 * if dragging and dropping, don't fire click or dbl click 1665 * event 1666 */ 1667 if((!go.drag.moving) || !go.drag.node) { 1668 shape._handleEvents('onclick', evt); 1669 1670 if(shape.inDoubleClickWindow) { 1671 shape._handleEvents('ondblclick', evt); 1672 } 1673 shape.inDoubleClickWindow = true; 1674 setTimeout(function() { 1675 shape.inDoubleClickWindow = false; 1676 }, this.dblClickWindow); 1677 } 1678 } 1679 return true; 1680 } 1681 1682 // handle touchstart 1683 else if(this.touchStart) { 1684 this.touchStart = false; 1685 shape._handleEvents('touchstart', evt); 1686 1687 if(el.ondbltap && shape.inDoubleClickWindow) { 1688 var events = el.ondbltap; 1689 for(var i = 0; i < events.length; i++) { 1690 events[i].handler.apply(shape, [evt]); 1691 } 1692 } 1693 1694 shape.inDoubleClickWindow = true; 1695 1696 setTimeout(function() { 1697 shape.inDoubleClickWindow = false; 1698 }, this.dblClickWindow); 1699 return true; 1700 } 1701 1702 // handle touchend 1703 else if(this.touchEnd) { 1704 this.touchEnd = false; 1705 shape._handleEvents('touchend', evt); 1706 return true; 1707 } 1708 1709 /* 1710 * NOTE: these event handlers require target shape 1711 * handling 1712 */ 1713 1714 // handle onmouseover 1715 else if(!isDragging && this._isNewTarget(shape, evt)) { 1716 /* 1717 * check to see if there are stored mouseout events first. 1718 * if there are, run those before running the onmouseover 1719 * events 1720 */ 1721 if(this.mouseoutShape) { 1722 this.mouseoverShape = shape; 1723 this.mouseoutShape._handleEvents('onmouseout', evt); 1724 this.mouseoverShape = undefined; 1725 } 1726 1727 shape._handleEvents('onmouseover', evt); 1728 this._setTarget(shape); 1729 return true; 1730 } 1731 1732 // handle mousemove and touchmove 1733 else if(!isDragging) { 1734 shape._handleEvents('onmousemove', evt); 1735 shape._handleEvents('touchmove', evt); 1736 return true; 1737 } 1738 } 1739 // handle mouseout condition 1740 else if(!isDragging && this.targetShape && this.targetShape._id === shape._id) { 1741 this._setTarget(undefined); 1742 this.mouseoutShape = shape; 1743 return true; 1744 } 1745 1746 return false; 1747 }, 1748 /** 1749 * set new target 1750 */ 1751 _setTarget: function(shape) { 1752 this.targetShape = shape; 1753 this.targetFound = true; 1754 }, 1755 /** 1756 * check if shape should be a new target 1757 */ 1758 _isNewTarget: function(shape, evt) { 1759 if(!this.targetShape || (!this.targetFound && shape._id !== this.targetShape._id)) { 1760 /* 1761 * check if old target has an onmouseout event listener 1762 */ 1763 if(this.targetShape) { 1764 var oldEl = this.targetShape.eventListeners; 1765 if(oldEl) { 1766 this.mouseoutShape = this.targetShape; 1767 } 1768 } 1769 return true; 1770 } 1771 else { 1772 return false; 1773 } 1774 }, 1775 /** 1776 * traverse container children 1777 * @param {Container} obj 1778 */ 1779 _traverseChildren: function(obj, evt) { 1780 var children = obj.children; 1781 // propapgate backwards through children 1782 for(var i = children.length - 1; i >= 0; i--) { 1783 var child = children[i]; 1784 if(child.attrs.listening) { 1785 if(child.nodeType === 'Shape') { 1786 var exit = this._detectEvent(child, evt); 1787 if(exit) { 1788 return true; 1789 } 1790 } 1791 else { 1792 var exit = this._traverseChildren(child, evt); 1793 if(exit) { 1794 return true; 1795 } 1796 } 1797 } 1798 } 1799 1800 return false; 1801 }, 1802 /** 1803 * handle incoming event 1804 * @param {Event} evt 1805 */ 1806 _handleStageEvent: function(evt) { 1807 var go = Kinetic.GlobalObject; 1808 if(!evt) { 1809 evt = window.event; 1810 } 1811 1812 this._setMousePosition(evt); 1813 this._setTouchPosition(evt); 1814 this.pathLayer.clear(); 1815 1816 /* 1817 * loop through layers. If at any point an event 1818 * is triggered, n is set to -1 which will break out of the 1819 * three nested loops 1820 */ 1821 this.targetFound = false; 1822 var shapeDetected = false; 1823 for(var n = this.children.length - 1; n >= 0; n--) { 1824 var layer = this.children[n]; 1825 if(layer.attrs.visible && n >= 0 && layer.attrs.listening) { 1826 if(this._traverseChildren(layer, evt)) { 1827 n = -1; 1828 shapeDetected = true; 1829 } 1830 } 1831 } 1832 1833 /* 1834 * if no shape was detected and a mouseout shape has been stored, 1835 * then run the onmouseout event handlers 1836 */ 1837 if(!shapeDetected && this.mouseoutShape) { 1838 this.mouseoutShape._handleEvents('onmouseout', evt); 1839 this.mouseoutShape = undefined; 1840 } 1841 }, 1842 /** 1843 * begin listening for events by adding event handlers 1844 * to the container 1845 */ 1846 _listen: function() { 1847 var that = this; 1848 1849 // desktop events 1850 this.content.addEventListener('mousedown', function(evt) { 1851 that.mouseDown = true; 1852 that._handleStageEvent(evt); 1853 }, false); 1854 1855 this.content.addEventListener('mousemove', function(evt) { 1856 that.mouseUp = false; 1857 that.mouseDown = false; 1858 that._handleStageEvent(evt); 1859 }, false); 1860 1861 this.content.addEventListener('mouseup', function(evt) { 1862 that.mouseUp = true; 1863 that.mouseDown = false; 1864 that._handleStageEvent(evt); 1865 1866 that.clickStart = false; 1867 }, false); 1868 1869 this.content.addEventListener('mouseover', function(evt) { 1870 that._handleStageEvent(evt); 1871 }, false); 1872 1873 this.content.addEventListener('mouseout', function(evt) { 1874 // if there's a current target shape, run mouseout handlers 1875 var targetShape = that.targetShape; 1876 if(targetShape) { 1877 targetShape._handleEvents('onmouseout', evt); 1878 that.targetShape = undefined; 1879 } 1880 that.mousePos = undefined; 1881 }, false); 1882 // mobile events 1883 this.content.addEventListener('touchstart', function(evt) { 1884 evt.preventDefault(); 1885 that.touchStart = true; 1886 that._handleStageEvent(evt); 1887 }, false); 1888 1889 this.content.addEventListener('touchmove', function(evt) { 1890 evt.preventDefault(); 1891 that._handleStageEvent(evt); 1892 }, false); 1893 1894 this.content.addEventListener('touchend', function(evt) { 1895 evt.preventDefault(); 1896 that.touchEnd = true; 1897 that._handleStageEvent(evt); 1898 }, false); 1899 }, 1900 /** 1901 * set mouse positon for desktop apps 1902 * @param {Event} evt 1903 */ 1904 _setMousePosition: function(evt) { 1905 var mouseX = evt.offsetX || (evt.clientX - this._getContentPosition().left + window.pageXOffset); 1906 var mouseY = evt.offsetY || (evt.clientY - this._getContentPosition().top + window.pageYOffset); 1907 this.mousePos = { 1908 x: mouseX, 1909 y: mouseY 1910 }; 1911 }, 1912 /** 1913 * set touch position for mobile apps 1914 * @param {Event} evt 1915 */ 1916 _setTouchPosition: function(evt) { 1917 if(evt.touches !== undefined && evt.touches.length === 1) {// Only deal with 1918 // one finger 1919 var touch = evt.touches[0]; 1920 // Get the information for finger #1 1921 var touchX = touch.clientX - this._getContentPosition().left + window.pageXOffset; 1922 var touchY = touch.clientY - this._getContentPosition().top + window.pageYOffset; 1923 1924 this.touchPos = { 1925 x: touchX, 1926 y: touchY 1927 }; 1928 } 1929 }, 1930 /** 1931 * get container position 1932 */ 1933 _getContentPosition: function() { 1934 var obj = this.content; 1935 var top = 0; 1936 var left = 0; 1937 while(obj && obj.tagName !== 'BODY') { 1938 top += obj.offsetTop - obj.scrollTop; 1939 left += obj.offsetLeft - obj.scrollLeft; 1940 obj = obj.offsetParent; 1941 } 1942 return { 1943 top: top, 1944 left: left 1945 }; 1946 }, 1947 /** 1948 * modify path context 1949 * @param {CanvasContext} context 1950 */ 1951 _modifyPathContext: function(context) { 1952 context.stroke = function() { 1953 }; 1954 context.fill = function() { 1955 }; 1956 context.fillRect = function(x, y, width, height) { 1957 context.rect(x, y, width, height); 1958 }; 1959 context.strokeRect = function(x, y, width, height) { 1960 context.rect(x, y, width, height); 1961 }; 1962 context.drawImage = function() { 1963 }; 1964 context.fillText = function() { 1965 }; 1966 context.strokeText = function() { 1967 }; 1968 }, 1969 /** 1970 * end drag and drop 1971 */ 1972 _endDrag: function(evt) { 1973 var go = Kinetic.GlobalObject; 1974 if(go.drag.node) { 1975 if(go.drag.moving) { 1976 go.drag.moving = false; 1977 go.drag.node._handleEvents('ondragend', evt); 1978 } 1979 } 1980 go.drag.node = undefined; 1981 }, 1982 /** 1983 * prepare drag and drop 1984 */ 1985 _prepareDrag: function() { 1986 var that = this; 1987 1988 this._onContent('mousemove touchmove', function(evt) { 1989 var go = Kinetic.GlobalObject; 1990 var node = go.drag.node; 1991 if(node) { 1992 var date = new Date(); 1993 var time = date.getTime(); 1994 1995 if(time - go.drag.lastDrawTime > go.dragTimeInterval) { 1996 go.drag.lastDrawTime = time; 1997 1998 var pos = that.getUserPosition(); 1999 var dc = node.attrs.dragConstraint; 2000 var db = node.attrs.dragBounds; 2001 var lastNodePos = { 2002 x: node.attrs.x, 2003 y: node.attrs.y 2004 }; 2005 2006 // default 2007 var newNodePos = { 2008 x: pos.x - go.drag.offset.x, 2009 y: pos.y - go.drag.offset.y 2010 }; 2011 2012 // bounds overrides 2013 if(db.left !== undefined && newNodePos.x < db.left) { 2014 newNodePos.x = db.left; 2015 } 2016 if(db.right !== undefined && newNodePos.x > db.right) { 2017 newNodePos.x = db.right; 2018 } 2019 if(db.top !== undefined && newNodePos.y < db.top) { 2020 newNodePos.y = db.top; 2021 } 2022 if(db.bottom !== undefined && newNodePos.y > db.bottom) { 2023 newNodePos.y = db.bottom; 2024 } 2025 2026 node.setAbsolutePosition(newNodePos); 2027 2028 // constraint overrides 2029 if(dc === 'horizontal') { 2030 node.attrs.y = lastNodePos.y; 2031 } 2032 else if(dc === 'vertical') { 2033 node.attrs.x = lastNodePos.x; 2034 } 2035 2036 go.drag.node.getLayer().draw(); 2037 2038 if(!go.drag.moving) { 2039 go.drag.moving = true; 2040 // execute dragstart events if defined 2041 go.drag.node._handleEvents('ondragstart', evt); 2042 } 2043 2044 // execute user defined ondragmove if defined 2045 go.drag.node._handleEvents('ondragmove', evt); 2046 } 2047 } 2048 }, false); 2049 2050 this._onContent('mouseup touchend mouseout', function(evt) { 2051 that._endDrag(evt); 2052 }); 2053 }, 2054 /** 2055 * build dom 2056 */ 2057 _buildDOM: function() { 2058 // content 2059 this.content.style.position = 'relative'; 2060 this.content.style.display = 'inline-block'; 2061 this.content.className = 'kineticjs-content'; 2062 this.container.appendChild(this.content); 2063 2064 // default layers 2065 this.bufferLayer = new Kinetic.Layer({ 2066 name: 'bufferLayer' 2067 }); 2068 this.pathLayer = new Kinetic.Layer({ 2069 name: 'pathLayer' 2070 }); 2071 2072 // set parents 2073 this.bufferLayer.parent = this; 2074 this.pathLayer.parent = this; 2075 2076 // customize back stage context 2077 this._modifyPathContext(this.pathLayer.context); 2078 2079 // hide canvases 2080 this.bufferLayer.getCanvas().style.display = 'none'; 2081 this.pathLayer.getCanvas().style.display = 'none'; 2082 2083 // add buffer layer 2084 this.bufferLayer.canvas.className = 'kineticjs-buffer-layer'; 2085 this.content.appendChild(this.bufferLayer.canvas); 2086 2087 // add path layer 2088 this.pathLayer.canvas.className = 'kineticjs-path-layer'; 2089 this.content.appendChild(this.pathLayer.canvas); 2090 2091 this.setSize(this.attrs.width, this.attrs.height); 2092 }, 2093 _addId: function(node) { 2094 if(node.attrs.id !== undefined) { 2095 this.ids[node.attrs.id] = node; 2096 } 2097 }, 2098 _removeId: function(node) { 2099 if(node.attrs.id !== undefined) { 2100 this.ids[node.attrs.id] = undefined; 2101 } 2102 }, 2103 _addName: function(node) { 2104 var name = node.attrs.name; 2105 if(name !== undefined) { 2106 if(this.names[name] === undefined) { 2107 this.names[name] = []; 2108 } 2109 this.names[name].push(node); 2110 } 2111 }, 2112 _removeName: function(node) { 2113 if(node.attrs.name !== undefined) { 2114 var nodes = this.names[node.attrs.name]; 2115 if(nodes !== undefined) { 2116 for(var n = 0; n < nodes.length; n++) { 2117 var no = nodes[n]; 2118 if(no._id === node._id) { 2119 nodes.splice(n, 1); 2120 } 2121 } 2122 } 2123 } 2124 }, 2125 /** 2126 * bind event listener to container DOM element 2127 * @param {String} typesStr 2128 * @param {function} handler 2129 */ 2130 _onContent: function(typesStr, handler) { 2131 var types = typesStr.split(' '); 2132 for(var n = 0; n < types.length; n++) { 2133 var baseEvent = types[n]; 2134 this.content.addEventListener(baseEvent, handler, false); 2135 } 2136 }, 2137 /** 2138 * set defaults 2139 */ 2140 _setDefaults: function() { 2141 this.clickStart = false; 2142 this.targetShape = undefined; 2143 this.targetFound = false; 2144 this.mouseoverShape = undefined; 2145 this.mouseoutShape = undefined; 2146 2147 // desktop flags 2148 this.mousePos = undefined; 2149 this.mouseDown = false; 2150 this.mouseUp = false; 2151 2152 // mobile flags 2153 this.touchPos = undefined; 2154 this.touchStart = false; 2155 this.touchEnd = false; 2156 2157 this.ids = {}; 2158 this.names = {}; 2159 this.anim = undefined; 2160 this.animRunning = false; 2161 } 2162 }; 2163 // Extend Container and Node 2164 Kinetic.GlobalObject.extend(Kinetic.Stage, Kinetic.Container); 2165 Kinetic.GlobalObject.extend(Kinetic.Stage, Kinetic.Node); 2166 2167 /////////////////////////////////////////////////////////////////////// 2168 // Layer 2169 /////////////////////////////////////////////////////////////////////// 2170 /** 2171 * Layer constructor. Layers are tied to their own canvas element and are used 2172 * to contain groups or shapes 2173 * @constructor 2174 * @augments Kinetic.Container 2175 * @augments Kinetic.Node 2176 * @param {Object} config 2177 */ 2178 Kinetic.Layer = function(config) { 2179 this.setDefaultAttrs({ 2180 throttle: 12 2181 }); 2182 2183 this.nodeType = 'Layer'; 2184 this.lastDrawTime = 0; 2185 this.beforeDrawFunc = undefined; 2186 this.afterDrawFunc = undefined; 2187 2188 this.canvas = document.createElement('canvas'); 2189 this.context = this.canvas.getContext('2d'); 2190 this.canvas.style.position = 'absolute'; 2191 2192 // call super constructors 2193 Kinetic.Container.apply(this, []); 2194 Kinetic.Node.apply(this, [config]); 2195 }; 2196 /* 2197 * Layer methods 2198 */ 2199 Kinetic.Layer.prototype = { 2200 /** 2201 * draw children nodes. this includes any groups 2202 * or shapes 2203 */ 2204 draw: function() { 2205 var throttle = this.attrs.throttle; 2206 var date = new Date(); 2207 var time = date.getTime(); 2208 var timeDiff = time - this.lastDrawTime; 2209 2210 if(timeDiff >= throttle) { 2211 this._draw(); 2212 this.lastDrawTime = time; 2213 if(this.drawTimeout !== undefined) { 2214 clearTimeout(this.drawTimeout); 2215 this.drawTimeout = undefined; 2216 } 2217 } 2218 /* 2219 * if we cannot draw the layer due to throttling, 2220 * try to redraw the layer in the near future 2221 */ 2222 else if(this.drawTimeout === undefined) { 2223 var that = this; 2224 /* 2225 * if timeout duration is too short, we will 2226 * get a lot of unecessary layer draws. Make sure 2227 * that the timeout is slightly more than the throttle 2228 * amount 2229 */ 2230 this.drawTimeout = setTimeout(function() { 2231 that.draw(); 2232 }, throttle + 10); 2233 } 2234 }, 2235 /** 2236 * set throttle 2237 * @param {Number} throttle in ms 2238 */ 2239 setThrottle: function(throttle) { 2240 this.attrs.throttle = throttle; 2241 }, 2242 /** 2243 * get throttle 2244 */ 2245 getThrottle: function() { 2246 return this.attrs.throttle; 2247 }, 2248 /** 2249 * set before draw function handler 2250 */ 2251 beforeDraw: function(func) { 2252 this.beforeDrawFunc = func; 2253 }, 2254 /** 2255 * set after draw function handler 2256 */ 2257 afterDraw: function(func) { 2258 this.afterDrawFunc = func; 2259 }, 2260 /** 2261 * clears the canvas context tied to the layer. Clearing 2262 * a layer does not remove its children. The nodes within 2263 * the layer will be redrawn whenever the .draw() method 2264 * is used again. 2265 */ 2266 clear: function() { 2267 var context = this.getContext(); 2268 var canvas = this.getCanvas(); 2269 context.clearRect(0, 0, canvas.width, canvas.height); 2270 }, 2271 /** 2272 * get layer canvas 2273 */ 2274 getCanvas: function() { 2275 return this.canvas; 2276 }, 2277 /** 2278 * get layer context 2279 */ 2280 getContext: function() { 2281 return this.context; 2282 }, 2283 /** 2284 * add a node to the layer. New nodes are always 2285 * placed at the top. 2286 * @param {Node} node 2287 */ 2288 add: function(child) { 2289 this._add(child); 2290 }, 2291 /** 2292 * remove a child from the layer 2293 * @param {Node} child 2294 */ 2295 remove: function(child) { 2296 this._remove(child); 2297 }, 2298 /** 2299 * private draw children 2300 */ 2301 _draw: function() { 2302 // before draw handler 2303 if(this.beforeDrawFunc !== undefined) { 2304 this.beforeDrawFunc(); 2305 } 2306 2307 this.clear(); 2308 if(this.attrs.visible) { 2309 this._drawChildren(); 2310 } 2311 2312 // after draw handler 2313 if(this.afterDrawFunc !== undefined) { 2314 this.afterDrawFunc(); 2315 } 2316 } 2317 }; 2318 // Extend Container and Node 2319 Kinetic.GlobalObject.extend(Kinetic.Layer, Kinetic.Container); 2320 Kinetic.GlobalObject.extend(Kinetic.Layer, Kinetic.Node); 2321 2322 /////////////////////////////////////////////////////////////////////// 2323 // Group 2324 /////////////////////////////////////////////////////////////////////// 2325 2326 /** 2327 * Group constructor. Groups are used to contain shapes or other groups. 2328 * @constructor 2329 * @augments Kinetic.Container 2330 * @augments Kinetic.Node 2331 * @param {Object} config 2332 */ 2333 Kinetic.Group = function(config) { 2334 this.nodeType = 'Group';; 2335 2336 // call super constructors 2337 Kinetic.Container.apply(this, []); 2338 Kinetic.Node.apply(this, [config]); 2339 }; 2340 /* 2341 * Group methods 2342 */ 2343 Kinetic.Group.prototype = { 2344 /** 2345 * add node to group 2346 * @param {Node} child 2347 */ 2348 add: function(child) { 2349 this._add(child); 2350 }, 2351 /** 2352 * remove a child node from the group 2353 * @param {Node} child 2354 */ 2355 remove: function(child) { 2356 this._remove(child); 2357 }, 2358 /** 2359 * draw children 2360 */ 2361 _draw: function() { 2362 if(this.attrs.visible) { 2363 this._drawChildren(); 2364 } 2365 } 2366 }; 2367 2368 // Extend Container and Node 2369 Kinetic.GlobalObject.extend(Kinetic.Group, Kinetic.Container); 2370 Kinetic.GlobalObject.extend(Kinetic.Group, Kinetic.Node); 2371 2372 /////////////////////////////////////////////////////////////////////// 2373 // Shape 2374 /////////////////////////////////////////////////////////////////////// 2375 /** 2376 * Shape constructor. Shapes are used to objectify drawing bits of a KineticJS 2377 * application 2378 * @constructor 2379 * @augments Kinetic.Node 2380 * @param {Object} config 2381 * @config {String|CanvasGradient|CanvasPattern} [fill] fill 2382 * @config {String} [stroke] stroke color 2383 * @config {Number} [strokeWidth] stroke width 2384 * @config {String} [lineJoin] line join. Can be "miter", "round", or "bevel". The default 2385 * is "miter" 2386 * @config {String} [detectionType] shape detection type. Can be "path" or "pixel". 2387 * The default is "path" because it performs better 2388 */ 2389 Kinetic.Shape = function(config) { 2390 this.setDefaultAttrs({ 2391 fill: undefined, 2392 stroke: undefined, 2393 strokeWidth: undefined, 2394 lineJoin: undefined, 2395 detectionType: 'path' 2396 }); 2397 2398 this.data = []; 2399 this.nodeType = 'Shape'; 2400 2401 // call super constructor 2402 Kinetic.Node.apply(this, [config]); 2403 }; 2404 /* 2405 * Shape methods 2406 */ 2407 Kinetic.Shape.prototype = { 2408 /** 2409 * get layer context where the shape is being drawn. When 2410 * the shape is being rendered, .getContext() returns the context of the 2411 * user created layer that contains the shape. When the event detection 2412 * engine is determining whether or not an event has occured on that shape, 2413 * .getContext() returns the context of the invisible path layer. 2414 */ 2415 getContext: function() { 2416 return this.tempLayer.getContext(); 2417 }, 2418 /** 2419 * get shape temp layer canvas 2420 */ 2421 getCanvas: function() { 2422 return this.tempLayer.getCanvas(); 2423 }, 2424 /** 2425 * helper method to stroke shape 2426 */ 2427 stroke: function() { 2428 var context = this.getContext(); 2429 2430 if(!!this.attrs.stroke || !!this.attrs.strokeWidth) { 2431 var stroke = !!this.attrs.stroke ? this.attrs.stroke : 'black'; 2432 var strokeWidth = !!this.attrs.strokeWidth ? this.attrs.strokeWidth : 2; 2433 2434 context.lineWidth = strokeWidth; 2435 context.strokeStyle = stroke; 2436 context.stroke(); 2437 } 2438 }, 2439 /** 2440 * helper method to fill and stroke a shape 2441 * based on its fill, stroke, and strokeWidth, properties 2442 */ 2443 fillStroke: function() { 2444 var context = this.getContext(); 2445 2446 /* 2447 * expect that fill, stroke, and strokeWidth could be 2448 * undfined, '', null, or 0. Use !! 2449 */ 2450 if(!!this.attrs.fill) { 2451 context.fillStyle = this.attrs.fill; 2452 context.fill(); 2453 } 2454 2455 this.stroke(); 2456 }, 2457 /** 2458 * helper method to set the line join of a shape 2459 * based on the lineJoin property 2460 */ 2461 applyLineJoin: function() { 2462 var context = this.getContext(); 2463 if(this.attrs.lineJoin !== undefined) { 2464 context.lineJoin = this.attrs.lineJoin; 2465 } 2466 }, 2467 /** 2468 * set fill which can be a color, gradient object, 2469 * or pattern object 2470 * @param {String|CanvasGradient|CanvasPattern} fill 2471 */ 2472 setFill: function(fill) { 2473 this.attrs.fill = fill; 2474 }, 2475 /** 2476 * get fill 2477 */ 2478 getFill: function() { 2479 return this.attrs.fill; 2480 }, 2481 /** 2482 * set stroke color 2483 * @param {String} stroke 2484 */ 2485 setStroke: function(stroke) { 2486 this.attrs.stroke = stroke; 2487 }, 2488 /** 2489 * get stroke color 2490 */ 2491 getStroke: function() { 2492 return this.attrs.stroke; 2493 }, 2494 /** 2495 * set line join 2496 * @param {String} lineJoin. Can be "miter", "round", or "bevel". The 2497 * default is "miter" 2498 */ 2499 setLineJoin: function(lineJoin) { 2500 this.attrs.lineJoin = lineJoin; 2501 }, 2502 /** 2503 * get line join 2504 */ 2505 getLineJoin: function() { 2506 return this.attrs.lineJoin; 2507 }, 2508 /** 2509 * set stroke width 2510 * @param {Number} strokeWidth 2511 */ 2512 setStrokeWidth: function(strokeWidth) { 2513 this.attrs.strokeWidth = strokeWidth; 2514 }, 2515 /** 2516 * get stroke width 2517 */ 2518 getStrokeWidth: function() { 2519 return this.attrs.strokeWidth; 2520 }, 2521 /** 2522 * set draw function 2523 * @param {Function} func drawing function 2524 */ 2525 setDrawFunc: function(func) { 2526 this.drawFunc = func; 2527 }, 2528 /** 2529 * save shape data when using pixel detection. 2530 */ 2531 saveData: function() { 2532 var stage = this.getStage(); 2533 var w = stage.attrs.width; 2534 var h = stage.attrs.height; 2535 2536 var bufferLayer = stage.bufferLayer; 2537 var bufferLayerContext = bufferLayer.getContext(); 2538 2539 bufferLayer.clear(); 2540 this._draw(bufferLayer); 2541 2542 var imageData = bufferLayerContext.getImageData(0, 0, w, h); 2543 this.data = imageData.data; 2544 }, 2545 /** 2546 * clear shape data 2547 */ 2548 clearData: function() { 2549 this.data = []; 2550 }, 2551 /** 2552 * determines if point is in the shape 2553 */ 2554 intersects: function() { 2555 var pos = Kinetic.GlobalObject._getPoint(arguments); 2556 var stage = this.getStage(); 2557 2558 if(this.attrs.detectionType === 'path') { 2559 var pathLayer = stage.pathLayer; 2560 var pathLayerContext = pathLayer.getContext(); 2561 2562 this._draw(pathLayer); 2563 2564 return pathLayerContext.isPointInPath(pos.x, pos.y); 2565 } 2566 else { 2567 var w = stage.attrs.width; 2568 var alpha = this.data[((w * pos.y) + pos.x) * 4 + 3]; 2569 return (alpha !== undefined && alpha !== 0); 2570 } 2571 }, 2572 /** 2573 * draw shape 2574 * @param {Layer} layer Layer that the shape will be drawn on 2575 */ 2576 _draw: function(layer) { 2577 if(layer !== undefined && this.drawFunc !== undefined) { 2578 var stage = layer.getStage(); 2579 var context = layer.getContext(); 2580 var family = []; 2581 var parent = this.parent; 2582 2583 family.unshift(this); 2584 while(parent) { 2585 family.unshift(parent); 2586 parent = parent.parent; 2587 } 2588 2589 context.save(); 2590 for(var n = 0; n < family.length; n++) { 2591 var node = family[n]; 2592 var t = node.getTransform(); 2593 2594 // center offset 2595 if(node.attrs.centerOffset.x !== 0 || node.attrs.centerOffset.y !== 0) { 2596 t.translate(-1 * node.attrs.centerOffset.x, -1 * node.attrs.centerOffset.y); 2597 } 2598 2599 var m = t.getMatrix(); 2600 context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); 2601 } 2602 2603 if(this.getAbsoluteAlpha() !== 1) { 2604 context.globalAlpha = this.getAbsoluteAlpha(); 2605 } 2606 2607 this.tempLayer = layer; 2608 this.drawFunc.call(this); 2609 context.restore(); 2610 } 2611 } 2612 }; 2613 // extend Node 2614 Kinetic.GlobalObject.extend(Kinetic.Shape, Kinetic.Node); 2615 2616 /////////////////////////////////////////////////////////////////////// 2617 // Rect 2618 /////////////////////////////////////////////////////////////////////// 2619 /** 2620 * Rect constructor 2621 * @constructor 2622 * @augments Kinetic.Shape 2623 * @param {Object} config 2624 */ 2625 Kinetic.Rect = function(config) { 2626 this.setDefaultAttrs({ 2627 width: 0, 2628 height: 0, 2629 cornerRadius: 0 2630 }); 2631 2632 this.shapeType = "Rect"; 2633 2634 config.drawFunc = function() { 2635 var context = this.getContext(); 2636 context.beginPath(); 2637 this.applyLineJoin(); 2638 if(this.attrs.cornerRadius === 0) { 2639 // simple rect - don't bother doing all that complicated maths stuff. 2640 context.rect(0, 0, this.attrs.width, this.attrs.height); 2641 } 2642 else { 2643 // arcTo would be nicer, but browser support is patchy (Opera) 2644 context.moveTo(this.attrs.cornerRadius, 0); 2645 context.lineTo(this.attrs.width - this.attrs.cornerRadius, 0); 2646 context.arc(this.attrs.width - this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI * 3 / 2, 0, false); 2647 context.lineTo(this.attrs.width, this.attrs.height - this.attrs.cornerRadius); 2648 context.arc(this.attrs.width - this.attrs.cornerRadius, this.attrs.height - this.attrs.cornerRadius, this.attrs.cornerRadius, 0, Math.PI / 2, false); 2649 context.lineTo(this.attrs.cornerRadius, this.attrs.height); 2650 context.arc(this.attrs.cornerRadius, this.attrs.height - this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI / 2, Math.PI, false); 2651 context.lineTo(0, this.attrs.cornerRadius); 2652 context.arc(this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI, Math.PI * 3 / 2, false); 2653 } 2654 context.closePath(); 2655 this.fillStroke(); 2656 }; 2657 // call super constructor 2658 Kinetic.Shape.apply(this, [config]); 2659 }; 2660 /* 2661 * Rect methods 2662 */ 2663 Kinetic.Rect.prototype = { 2664 /** 2665 * set width 2666 * @param {Number} width 2667 */ 2668 setWidth: function(width) { 2669 this.attrs.width = width; 2670 }, 2671 /** 2672 * get width 2673 */ 2674 getWidth: function() { 2675 return this.attrs.width; 2676 }, 2677 /** 2678 * set height 2679 * @param {Number} height 2680 */ 2681 setHeight: function(height) { 2682 this.attrs.height = height; 2683 }, 2684 /** 2685 * get height 2686 */ 2687 getHeight: function() { 2688 return this.attrs.height; 2689 }, 2690 /** 2691 * set width and height 2692 * @param {Number} width 2693 * @param {Number} height 2694 */ 2695 setSize: function(width, height) { 2696 this.attrs.width = width; 2697 this.attrs.height = height; 2698 }, 2699 /** 2700 * return rect size 2701 */ 2702 getSize: function() { 2703 return { 2704 width: this.attrs.width, 2705 height: this.attrs.height 2706 }; 2707 }, 2708 /** 2709 * set corner radius 2710 * @param {Number} radius 2711 */ 2712 setCornerRadius: function(radius) { 2713 this.attrs.cornerRadius = radius; 2714 }, 2715 /** 2716 * get corner radius 2717 */ 2718 getCornerRadius: function() { 2719 return this.attrs.cornerRadius; 2720 }, 2721 }; 2722 2723 // extend Shape 2724 Kinetic.GlobalObject.extend(Kinetic.Rect, Kinetic.Shape); 2725 2726 /////////////////////////////////////////////////////////////////////// 2727 // Circle 2728 /////////////////////////////////////////////////////////////////////// 2729 /** 2730 * Circle constructor 2731 * @constructor 2732 * @augments Kinetic.Shape 2733 * @param {Object} config 2734 */ 2735 Kinetic.Circle = function(config) { 2736 this.setDefaultAttrs({ 2737 radius: 0 2738 }); 2739 2740 this.shapeType = "Circle"; 2741 2742 config.drawFunc = function() { 2743 var canvas = this.getCanvas(); 2744 var context = this.getContext(); 2745 context.beginPath(); 2746 this.applyLineJoin(); 2747 context.arc(0, 0, this.attrs.radius, 0, Math.PI * 2, true); 2748 context.closePath(); 2749 this.fillStroke(); 2750 }; 2751 // call super constructor 2752 Kinetic.Shape.apply(this, [config]); 2753 }; 2754 /* 2755 * Circle methods 2756 */ 2757 Kinetic.Circle.prototype = { 2758 /** 2759 * set radius 2760 * @param {Number} radius 2761 */ 2762 setRadius: function(radius) { 2763 this.attrs.radius = radius; 2764 }, 2765 /** 2766 * get radius 2767 */ 2768 getRadius: function() { 2769 return this.attrs.radius; 2770 } 2771 }; 2772 2773 // extend Shape 2774 Kinetic.GlobalObject.extend(Kinetic.Circle, Kinetic.Shape); 2775 2776 /////////////////////////////////////////////////////////////////////// 2777 // Image 2778 /////////////////////////////////////////////////////////////////////// 2779 /** 2780 * Image constructor 2781 * @constructor 2782 * @augments Kinetic.Shape 2783 * @param {Object} config 2784 */ 2785 Kinetic.Image = function(config) { 2786 this.setDefaultAttrs({ 2787 crop: { 2788 x: 0, 2789 y: 0, 2790 width: undefined, 2791 height: undefined 2792 } 2793 }); 2794 2795 this.shapeType = "Image"; 2796 config.drawFunc = function() { 2797 if(this.image !== undefined) { 2798 var width = this.attrs.width !== undefined ? this.attrs.width : this.image.width; 2799 var height = this.attrs.height !== undefined ? this.attrs.height : this.image.height; 2800 var cropX = this.attrs.crop.x; 2801 var cropY = this.attrs.crop.y; 2802 var cropWidth = this.attrs.crop.width; 2803 var cropHeight = this.attrs.crop.height; 2804 var canvas = this.getCanvas(); 2805 var context = this.getContext(); 2806 2807 context.beginPath(); 2808 this.applyLineJoin(); 2809 context.rect(0, 0, width, height); 2810 context.closePath(); 2811 this.fillStroke(); 2812 2813 // if cropping 2814 if(cropWidth !== undefined && cropHeight !== undefined) { 2815 context.drawImage(this.image, cropX, cropY, cropWidth, cropHeight, 0, 0, width, height); 2816 } 2817 // no cropping 2818 else { 2819 context.drawImage(this.image, 0, 0, width, height); 2820 } 2821 } 2822 }; 2823 // call super constructor 2824 Kinetic.Shape.apply(this, [config]); 2825 }; 2826 /* 2827 * Image methods 2828 */ 2829 Kinetic.Image.prototype = { 2830 /** 2831 * set image 2832 * @param {ImageObject} image 2833 */ 2834 setImage: function(image) { 2835 this.image = image; 2836 }, 2837 /** 2838 * get image 2839 */ 2840 getImage: function() { 2841 return this.image; 2842 }, 2843 /** 2844 * set width 2845 * @param {Number} width 2846 */ 2847 setWidth: function(width) { 2848 this.attrs.width = width; 2849 }, 2850 /** 2851 * get width 2852 */ 2853 getWidth: function() { 2854 return this.attrs.width; 2855 }, 2856 /** 2857 * set height 2858 * @param {Number} height 2859 */ 2860 setHeight: function(height) { 2861 this.attrs.height = height; 2862 }, 2863 /** 2864 * get height 2865 */ 2866 getHeight: function() { 2867 return this.attrs.height; 2868 }, 2869 /** 2870 * set width and height 2871 * @param {Number} width 2872 * @param {Number} height 2873 */ 2874 setSize: function(width, height) { 2875 this.attrs.width = width; 2876 this.attrs.height = height; 2877 }, 2878 /** 2879 * return image size 2880 */ 2881 getSize: function() { 2882 return { 2883 width: this.attrs.width, 2884 height: this.attrs.height 2885 }; 2886 }, 2887 /** 2888 * return cropping 2889 */ 2890 getCrop: function() { 2891 return this.attrs.crop; 2892 }, 2893 /** 2894 * set cropping 2895 * @param {Object} crop 2896 * @config {Number} [x] crop x 2897 * @config {Number} [y] crop y 2898 * @config {Number} [width] crop width 2899 * @config {Number} [height] crop height 2900 */ 2901 setCrop: function(config) { 2902 var c = {}; 2903 c.crop = config; 2904 this.setAttrs(c); 2905 } 2906 }; 2907 // extend Shape 2908 Kinetic.GlobalObject.extend(Kinetic.Image, Kinetic.Shape); 2909 2910 /////////////////////////////////////////////////////////////////////// 2911 // Polygon 2912 /////////////////////////////////////////////////////////////////////// 2913 /** 2914 * Polygon constructor. Polygons are defined by an array of points 2915 * @constructor 2916 * @augments Kinetic.Shape 2917 * @param {Object} config 2918 */ 2919 Kinetic.Polygon = function(config) { 2920 this.setDefaultAttrs({ 2921 points: {} 2922 }); 2923 2924 this.shapeType = "Polygon"; 2925 config.drawFunc = function() { 2926 var context = this.getContext(); 2927 context.beginPath(); 2928 this.applyLineJoin(); 2929 context.moveTo(this.attrs.points[0].x, this.attrs.points[0].y); 2930 for(var n = 1; n < this.attrs.points.length; n++) { 2931 context.lineTo(this.attrs.points[n].x, this.attrs.points[n].y); 2932 } 2933 context.closePath(); 2934 this.fillStroke(); 2935 }; 2936 // call super constructor 2937 Kinetic.Shape.apply(this, [config]); 2938 }; 2939 /* 2940 * Polygon methods 2941 */ 2942 Kinetic.Polygon.prototype = { 2943 /** 2944 * set points array 2945 * @param {Array} points 2946 */ 2947 setPoints: function(points) { 2948 this.attrs.points = points; 2949 }, 2950 /** 2951 * get points array 2952 */ 2953 getPoints: function() { 2954 return this.attrs.points; 2955 } 2956 }; 2957 2958 // extend Shape 2959 Kinetic.GlobalObject.extend(Kinetic.Polygon, Kinetic.Shape); 2960 2961 /////////////////////////////////////////////////////////////////////// 2962 // RegularPolygon 2963 /////////////////////////////////////////////////////////////////////// 2964 /** 2965 * RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc. 2966 * @constructor 2967 * @augments Kinetic.Shape 2968 * @param {Object} config 2969 */ 2970 Kinetic.RegularPolygon = function(config) { 2971 this.setDefaultAttrs({ 2972 radius: 0, 2973 sides: 0 2974 }); 2975 2976 this.shapeType = "RegularPolygon"; 2977 config.drawFunc = function() { 2978 var context = this.getContext(); 2979 context.beginPath(); 2980 this.applyLineJoin(); 2981 context.moveTo(0, 0 - this.attrs.radius); 2982 2983 for(var n = 1; n < this.attrs.sides; n++) { 2984 var x = this.attrs.radius * Math.sin(n * 2 * Math.PI / this.attrs.sides); 2985 var y = -1 * this.attrs.radius * Math.cos(n * 2 * Math.PI / this.attrs.sides); 2986 context.lineTo(x, y); 2987 } 2988 context.closePath(); 2989 this.fillStroke(); 2990 }; 2991 // call super constructor 2992 Kinetic.Shape.apply(this, [config]); 2993 }; 2994 /* 2995 * RegularPolygon methods 2996 */ 2997 Kinetic.RegularPolygon.prototype = { 2998 /** 2999 * set radius 3000 * @param {Number} radius 3001 */ 3002 setRadius: function(radius) { 3003 this.attrs.radius = radius; 3004 }, 3005 /** 3006 * get radius 3007 */ 3008 getRadius: function() { 3009 return this.attrs.radius; 3010 }, 3011 /** 3012 * set number of sides 3013 * @param {int} sides 3014 */ 3015 setSides: function(sides) { 3016 this.attrs.sides = sides; 3017 }, 3018 /** 3019 * get number of sides 3020 */ 3021 getSides: function() { 3022 return this.attrs.sides; 3023 } 3024 }; 3025 3026 // extend Shape 3027 Kinetic.GlobalObject.extend(Kinetic.RegularPolygon, Kinetic.Shape); 3028 3029 /////////////////////////////////////////////////////////////////////// 3030 // Star 3031 /////////////////////////////////////////////////////////////////////// 3032 /** 3033 * Star constructor 3034 * @constructor 3035 * @augments Kinetic.Shape 3036 * @param {Object} config 3037 */ 3038 Kinetic.Star = function(config) { 3039 this.setDefaultAttrs({ 3040 points: [], 3041 innerRadius: 0, 3042 outerRadius: 0 3043 }); 3044 3045 this.shapeType = "Star"; 3046 config.drawFunc = function() { 3047 var context = this.getContext(); 3048 context.beginPath(); 3049 this.applyLineJoin(); 3050 context.moveTo(0, 0 - this.attrs.outerRadius); 3051 3052 for(var n = 1; n < this.attrs.points * 2; n++) { 3053 var radius = n % 2 === 0 ? this.attrs.outerRadius : this.attrs.innerRadius; 3054 var x = radius * Math.sin(n * Math.PI / this.attrs.points); 3055 var y = -1 * radius * Math.cos(n * Math.PI / this.attrs.points); 3056 context.lineTo(x, y); 3057 } 3058 context.closePath(); 3059 this.fillStroke(); 3060 }; 3061 // call super constructor 3062 Kinetic.Shape.apply(this, [config]); 3063 }; 3064 /* 3065 * Star methods 3066 */ 3067 Kinetic.Star.prototype = { 3068 /** 3069 * set points array 3070 * @param {Array} points 3071 */ 3072 setPoints: function(points) { 3073 this.attrs.points = points; 3074 }, 3075 /** 3076 * get points array 3077 */ 3078 getPoints: function() { 3079 return this.attrs.points; 3080 }, 3081 /** 3082 * set outer radius 3083 * @param {Number} radius 3084 */ 3085 setOuterRadius: function(radius) { 3086 this.attrs.outerRadius = radius; 3087 }, 3088 /** 3089 * get outer radius 3090 */ 3091 getOuterRadius: function() { 3092 return this.attrs.outerRadius; 3093 }, 3094 /** 3095 * set inner radius 3096 * @param {Number} radius 3097 */ 3098 setInnerRadius: function(radius) { 3099 this.attrs.innerRadius = radius; 3100 }, 3101 /** 3102 * get inner radius 3103 */ 3104 getInnerRadius: function() { 3105 return this.attrs.innerRadius; 3106 } 3107 }; 3108 // extend Shape 3109 Kinetic.GlobalObject.extend(Kinetic.Star, Kinetic.Shape); 3110 3111 /////////////////////////////////////////////////////////////////////// 3112 // Text 3113 /////////////////////////////////////////////////////////////////////// 3114 /** 3115 * Text constructor 3116 * @constructor 3117 * @augments Kinetic.Shape 3118 * @param {Object} config 3119 */ 3120 Kinetic.Text = function(config) { 3121 this.setDefaultAttrs({ 3122 fontFamily: 'Calibri', 3123 text: '', 3124 fontSize: 12, 3125 fill: undefined, 3126 textStroke: undefined, 3127 textStrokeWidth: undefined, 3128 align: 'left', 3129 verticalAlign: 'top', 3130 padding: 0, 3131 fontStyle: 'normal' 3132 }); 3133 3134 this.shapeType = "Text"; 3135 3136 config.drawFunc = function() { 3137 var context = this.getContext(); 3138 context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; 3139 context.textBaseline = 'middle'; 3140 var textHeight = this.getTextHeight(); 3141 var textWidth = this.getTextWidth(); 3142 var p = this.attrs.padding; 3143 var x = 0; 3144 var y = 0; 3145 3146 switch (this.attrs.align) { 3147 case 'center': 3148 x = textWidth / -2 - p; 3149 break; 3150 case 'right': 3151 x = -1 * textWidth - p; 3152 break; 3153 } 3154 3155 switch (this.attrs.verticalAlign) { 3156 case 'middle': 3157 y = textHeight / -2 - p; 3158 break; 3159 case 'bottom': 3160 y = -1 * textHeight - p; 3161 break; 3162 } 3163 3164 // draw path 3165 context.save(); 3166 context.beginPath(); 3167 this.applyLineJoin(); 3168 context.rect(x, y, textWidth + p * 2, textHeight + p * 2); 3169 context.closePath(); 3170 this.fillStroke(); 3171 context.restore(); 3172 3173 var tx = p + x; 3174 var ty = textHeight / 2 + p + y; 3175 3176 // draw text 3177 if(this.attrs.textFill !== undefined) { 3178 context.fillStyle = this.attrs.textFill; 3179 context.fillText(this.attrs.text, tx, ty); 3180 } 3181 if(this.attrs.textStroke !== undefined || this.attrs.textStrokeWidth !== undefined) { 3182 // defaults 3183 if(this.attrs.textStroke === undefined) { 3184 this.attrs.textStroke = 'black'; 3185 } 3186 else if(this.attrs.textStrokeWidth === undefined) { 3187 this.attrs.textStrokeWidth = 2; 3188 } 3189 context.lineWidth = this.attrs.textStrokeWidth; 3190 context.strokeStyle = this.attrs.textStroke; 3191 context.strokeText(this.attrs.text, tx, ty); 3192 } 3193 }; 3194 // call super constructor 3195 Kinetic.Shape.apply(this, [config]); 3196 }; 3197 /* 3198 * Text methods 3199 */ 3200 Kinetic.Text.prototype = { 3201 /** 3202 * set font family 3203 * @param {String} fontFamily 3204 */ 3205 setFontFamily: function(fontFamily) { 3206 this.attrs.fontFamily = fontFamily; 3207 }, 3208 /** 3209 * get font family 3210 */ 3211 getFontFamily: function() { 3212 return this.attrs.fontFamily; 3213 }, 3214 /** 3215 * set font size 3216 * @param {int} fontSize 3217 */ 3218 setFontSize: function(fontSize) { 3219 this.attrs.fontSize = fontSize; 3220 }, 3221 /** 3222 * get font size 3223 */ 3224 getFontSize: function() { 3225 return this.attrs.fontSize; 3226 }, 3227 /** 3228 * set font style. Can be "normal", "italic", or "bold". "normal" is the default. 3229 * @param {String} fontStyle 3230 */ 3231 setFontStyle: function(fontStyle) { 3232 this.attrs.fontStyle = fontStyle; 3233 }, 3234 /** 3235 * get font style 3236 */ 3237 getFontStyle: function() { 3238 return this.attrs.fontStyle; 3239 }, 3240 /** 3241 * set text fill color 3242 * @param {String} textFill 3243 */ 3244 setTextFill: function(textFill) { 3245 this.attrs.textFill = textFill; 3246 }, 3247 /** 3248 * get text fill color 3249 */ 3250 getTextFill: function() { 3251 return this.attrs.textFill; 3252 }, 3253 /** 3254 * set text stroke color 3255 * @param {String} textStroke 3256 */ 3257 setTextStroke: function(textStroke) { 3258 this.attrs.textStroke = textStroke; 3259 }, 3260 /** 3261 * get text stroke color 3262 */ 3263 getTextStroke: function() { 3264 return this.attrs.textStroke; 3265 }, 3266 /** 3267 * set text stroke width 3268 * @param {int} textStrokeWidth 3269 */ 3270 setTextStrokeWidth: function(textStrokeWidth) { 3271 this.attrs.textStrokeWidth = textStrokeWidth; 3272 }, 3273 /** 3274 * get text stroke width 3275 */ 3276 getTextStrokeWidth: function() { 3277 return this.attrs.textStrokeWidth; 3278 }, 3279 /** 3280 * set padding 3281 * @param {int} padding 3282 */ 3283 setPadding: function(padding) { 3284 this.attrs.padding = padding; 3285 }, 3286 /** 3287 * get padding 3288 */ 3289 getPadding: function() { 3290 return this.attrs.padding; 3291 }, 3292 /** 3293 * set horizontal align of text 3294 * @param {String} align align can be 'left', 'center', or 'right' 3295 */ 3296 setAlign: function(align) { 3297 this.attrs.align = align; 3298 }, 3299 /** 3300 * get horizontal align 3301 */ 3302 getAlign: function() { 3303 return this.attrs.align; 3304 }, 3305 /** 3306 * set vertical align of text 3307 * @param {String} verticalAlign verticalAlign can be "top", "middle", or "bottom" 3308 */ 3309 setVerticalAlign: function(verticalAlign) { 3310 this.attrs.verticalAlign = verticalAlign; 3311 }, 3312 /** 3313 * get vertical align 3314 */ 3315 getVerticalAlign: function() { 3316 return this.attrs.verticalAlign; 3317 }, 3318 /** 3319 * set text 3320 * @param {String} text 3321 */ 3322 setText: function(text) { 3323 this.attrs.text = text; 3324 }, 3325 /** 3326 * get text 3327 */ 3328 getText: function() { 3329 return this.attrs.text; 3330 }, 3331 /** 3332 * get text width in pixels 3333 */ 3334 getTextWidth: function() { 3335 return this.getTextSize().width; 3336 }, 3337 /** 3338 * get text height in pixels 3339 */ 3340 getTextHeight: function() { 3341 return this.getTextSize().height; 3342 }, 3343 /** 3344 * get text size in pixels 3345 */ 3346 getTextSize: function() { 3347 var context = this.getContext(); 3348 context.save(); 3349 context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; 3350 var metrics = context.measureText(this.attrs.text); 3351 context.restore(); 3352 return { 3353 width: metrics.width, 3354 height: parseInt(this.attrs.fontSize, 10) 3355 }; 3356 } 3357 }; 3358 // extend Shape 3359 Kinetic.GlobalObject.extend(Kinetic.Text, Kinetic.Shape); 3360 3361 /////////////////////////////////////////////////////////////////////// 3362 // Line 3363 /////////////////////////////////////////////////////////////////////// 3364 /** 3365 * Line constructor. Lines are defined by an array of points 3366 * @constructor 3367 * @augments Kinetic.Shape 3368 * @param {Object} config 3369 */ 3370 Kinetic.Line = function(config) { 3371 this.setDefaultAttrs({ 3372 points: {}, 3373 lineCap: 'butt' 3374 }); 3375 3376 this.shapeType = "Line"; 3377 config.drawFunc = function() { 3378 var context = this.getContext(); 3379 context.beginPath(); 3380 this.applyLineJoin(); 3381 context.moveTo(this.attrs.points[0].x, this.attrs.points[0].y); 3382 for(var n = 1; n < this.attrs.points.length; n++) { 3383 context.lineTo(this.attrs.points[n].x, this.attrs.points[n].y); 3384 } 3385 3386 if(!!this.attrs.lineCap) { 3387 context.lineCap = this.attrs.lineCap; 3388 } 3389 this.stroke(); 3390 }; 3391 // call super constructor 3392 Kinetic.Shape.apply(this, [config]); 3393 }; 3394 /* 3395 * Line methods 3396 */ 3397 Kinetic.Line.prototype = { 3398 /** 3399 * set points array 3400 * @param {Array} points 3401 */ 3402 setPoints: function(points) { 3403 this.attrs.points = points; 3404 }, 3405 /** 3406 * get points array 3407 */ 3408 getPoints: function() { 3409 return this.attrs.points; 3410 }, 3411 /** 3412 * set line cap. Can be butt, round, or square 3413 * @param {String} lineCap 3414 */ 3415 setLineCap: function(lineCap) { 3416 this.attrs.lineCap = lineCap; 3417 }, 3418 /** 3419 * get line cap 3420 */ 3421 getLineCap: function() { 3422 return this.attrs.lineCap; 3423 } 3424 }; 3425 3426 // extend Shape 3427 Kinetic.GlobalObject.extend(Kinetic.Line, Kinetic.Shape); 3428 3429 /* 3430 * Last updated November 2011 3431 * By Simon Sarris 3432 * www.simonsarris.com 3433 * sarris@acm.org 3434 * 3435 * Free to use and distribute at will 3436 * So long as you are nice to people, etc 3437 */ 3438 3439 /* 3440 * The usage of this class was inspired by some of the work done by a forked 3441 * project, KineticJS-Ext by Wappworks, which is based on Simon's Transform 3442 * class. 3443 */ 3444 3445 /** 3446 * Matrix object 3447 */ 3448 Kinetic.Transform = function() { 3449 this.m = [1, 0, 0, 1, 0, 0]; 3450 } 3451 3452 Kinetic.Transform.prototype = { 3453 /** 3454 * Apply translation 3455 * @param {Number} x 3456 * @param {Number} y 3457 */ 3458 translate: function(x, y) { 3459 this.m[4] += this.m[0] * x + this.m[2] * y; 3460 this.m[5] += this.m[1] * x + this.m[3] * y; 3461 }, 3462 /** 3463 * Apply scale 3464 * @param {Number} sx 3465 * @param {Number} sy 3466 */ 3467 scale: function(sx, sy) { 3468 this.m[0] *= sx; 3469 this.m[1] *= sx; 3470 this.m[2] *= sy; 3471 this.m[3] *= sy; 3472 }, 3473 /** 3474 * Apply rotation 3475 * @param {Number} rad Angle in radians 3476 */ 3477 rotate: function(rad) { 3478 var c = Math.cos(rad); 3479 var s = Math.sin(rad); 3480 var m11 = this.m[0] * c + this.m[2] * s; 3481 var m12 = this.m[1] * c + this.m[3] * s; 3482 var m21 = this.m[0] * -s + this.m[2] * c; 3483 var m22 = this.m[1] * -s + this.m[3] * c; 3484 this.m[0] = m11; 3485 this.m[1] = m12; 3486 this.m[2] = m21; 3487 this.m[3] = m22; 3488 }, 3489 /** 3490 * Returns the translation 3491 * @returns {Object} 2D point(x, y) 3492 */ 3493 getTranslation: function() { 3494 return { 3495 x: this.m[4], 3496 y: this.m[5] 3497 }; 3498 }, 3499 /** 3500 * Transform multiplication 3501 * @param {Kinetic.Transform} matrix 3502 */ 3503 multiply: function(matrix) { 3504 var m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1]; 3505 var m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1]; 3506 3507 var m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3]; 3508 var m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3]; 3509 3510 var dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4]; 3511 var dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5]; 3512 3513 this.m[0] = m11; 3514 this.m[1] = m12; 3515 this.m[2] = m21; 3516 this.m[3] = m22; 3517 this.m[4] = dx; 3518 this.m[5] = dy; 3519 }, 3520 /** 3521 * Invert the matrix 3522 */ 3523 invert: function() { 3524 var d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); 3525 var m0 = this.m[3] * d; 3526 var m1 = -this.m[1] * d; 3527 var m2 = -this.m[2] * d; 3528 var m3 = this.m[0] * d; 3529 var m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); 3530 var m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); 3531 this.m[0] = m0; 3532 this.m[1] = m1; 3533 this.m[2] = m2; 3534 this.m[3] = m3; 3535 this.m[4] = m4; 3536 this.m[5] = m5; 3537 }, 3538 /** 3539 * return matrix 3540 */ 3541 getMatrix: function() { 3542 return this.m; 3543 } 3544 }; 3545 3546 /* 3547 * The Tween class was ported from an Adobe Flash Tween library 3548 * to JavaScript by Xaric. In the context of KineticJS, a Tween is 3549 * an animation of a single Node property. A Transition is a set of 3550 * multiple tweens 3551 */ 3552 3553 /** 3554 * Transition constructor. KineticJS transitions contain 3555 * multiple Tweens 3556 */ 3557 Kinetic.Transition = function(node, config) { 3558 this.node = node; 3559 this.config = config; 3560 this.tweens = []; 3561 3562 // add tween for each property 3563 for(var key in config) { 3564 if(key !== 'duration' && key !== 'easing' && key !== 'callback') { 3565 if(config[key].x === undefined && config[key].y === undefined) { 3566 this.add(this._getTween(key, config)); 3567 } 3568 if(config[key].x !== undefined) { 3569 this.add(this._getComponentTween(key, 'x', config)); 3570 } 3571 if(config[key].y !== undefined) { 3572 this.add(this._getComponentTween(key, 'y', config)); 3573 } 3574 } 3575 } 3576 3577 var finishedTweens = 0; 3578 var that = this; 3579 for(var n = 0; n < this.tweens.length; n++) { 3580 var tween = this.tweens[n]; 3581 tween.onFinished = function() { 3582 finishedTweens++; 3583 if(finishedTweens >= that.tweens.length) { 3584 that.onFinished(); 3585 } 3586 }; 3587 } 3588 }; 3589 /* 3590 * Transition methods 3591 */ 3592 Kinetic.Transition.prototype = { 3593 /** 3594 * add tween to tweens array 3595 * @param {Kinetic.Tween} tween 3596 */ 3597 add: function(tween) { 3598 this.tweens.push(tween); 3599 }, 3600 /** 3601 * start transition 3602 */ 3603 start: function() { 3604 for(var n = 0; n < this.tweens.length; n++) { 3605 this.tweens[n].start(); 3606 } 3607 }, 3608 /** 3609 * onEnterFrame 3610 */ 3611 onEnterFrame: function() { 3612 for(var n = 0; n < this.tweens.length; n++) { 3613 this.tweens[n].onEnterFrame(); 3614 } 3615 }, 3616 /** 3617 * stop transition 3618 */ 3619 stop: function() { 3620 for(var n = 0; n < this.tweens.length; n++) { 3621 this.tweens[n].stop(); 3622 } 3623 }, 3624 /** 3625 * resume transition 3626 */ 3627 resume: function() { 3628 for(var n = 0; n < this.tweens.length; n++) { 3629 this.tweens[n].resume(); 3630 } 3631 }, 3632 _getTween: function(key) { 3633 var config = this.config; 3634 var node = this.node; 3635 var easing = config.easing; 3636 if(easing === undefined) { 3637 easing = 'linear'; 3638 } 3639 3640 var tween = new Kinetic.Tween(node, function(i) { 3641 node.attrs[key] = i; 3642 }, Kinetic.Tweens[easing], node.attrs[key], config[key], config.duration); 3643 3644 return tween; 3645 }, 3646 _getComponentTween: function(key, prop) { 3647 var config = this.config; 3648 var node = this.node; 3649 var easing = config.easing; 3650 if(easing === undefined) { 3651 easing = 'linear'; 3652 } 3653 3654 var tween = new Kinetic.Tween(node, function(i) { 3655 node.attrs[key][prop] = i; 3656 }, Kinetic.Tweens[easing], node.attrs[key][prop], config[key][prop], config.duration); 3657 3658 return tween; 3659 }, 3660 }; 3661 3662 /** 3663 * Tween constructor 3664 */ 3665 Kinetic.Tween = function(obj, propFunc, func, begin, finish, duration) { 3666 this._listeners = []; 3667 this.addListener(this); 3668 this.obj = obj; 3669 this.propFunc = propFunc; 3670 this.begin = begin; 3671 this._pos = begin; 3672 this.setDuration(duration); 3673 this.isPlaying = false; 3674 this._change = 0; 3675 this.prevTime = 0; 3676 this.prevPos = 0; 3677 this.looping = false; 3678 this._time = 0; 3679 this._position = 0; 3680 this._startTime = 0; 3681 this._finish = 0; 3682 this.name = ''; 3683 this.func = func; 3684 this.setFinish(finish); 3685 }; 3686 /* 3687 * Tween methods 3688 */ 3689 Kinetic.Tween.prototype = { 3690 setTime: function(t) { 3691 this.prevTime = this._time; 3692 if(t > this.getDuration()) { 3693 if(this.looping) { 3694 this.rewind(t - this._duration); 3695 this.update(); 3696 this.broadcastMessage('onLooped', { 3697 target: this, 3698 type: 'onLooped' 3699 }); 3700 } 3701 else { 3702 this._time = this._duration; 3703 this.update(); 3704 this.stop(); 3705 this.broadcastMessage('onFinished', { 3706 target: this, 3707 type: 'onFinished' 3708 }); 3709 } 3710 } 3711 else if(t < 0) { 3712 this.rewind(); 3713 this.update(); 3714 } 3715 else { 3716 this._time = t; 3717 this.update(); 3718 } 3719 }, 3720 getTime: function() { 3721 return this._time; 3722 }, 3723 setDuration: function(d) { 3724 this._duration = (d === null || d <= 0) ? 100000 : d; 3725 }, 3726 getDuration: function() { 3727 return this._duration; 3728 }, 3729 setPosition: function(p) { 3730 this.prevPos = this._pos; 3731 //var a = this.suffixe != '' ? this.suffixe : ''; 3732 this.propFunc(p); 3733 //+ a; 3734 //this.obj(Math.round(p)); 3735 this._pos = p; 3736 this.broadcastMessage('onChanged', { 3737 target: this, 3738 type: 'onChanged' 3739 }); 3740 }, 3741 getPosition: function(t) { 3742 if(t === undefined) { 3743 t = this._time; 3744 } 3745 return this.func(t, this.begin, this._change, this._duration); 3746 }, 3747 setFinish: function(f) { 3748 this._change = f - this.begin; 3749 }, 3750 getFinish: function() { 3751 return this.begin + this._change; 3752 }, 3753 start: function() { 3754 this.rewind(); 3755 this.startEnterFrame(); 3756 this.broadcastMessage('onStarted', { 3757 target: this, 3758 type: 'onStarted' 3759 }); 3760 }, 3761 rewind: function(t) { 3762 this.stop(); 3763 this._time = (t === undefined) ? 0 : t; 3764 this.fixTime(); 3765 this.update(); 3766 }, 3767 fforward: function() { 3768 this._time = this._duration; 3769 this.fixTime(); 3770 this.update(); 3771 }, 3772 update: function() { 3773 this.setPosition(this.getPosition(this._time)); 3774 }, 3775 startEnterFrame: function() { 3776 this.stopEnterFrame(); 3777 this.isPlaying = true; 3778 this.onEnterFrame(); 3779 }, 3780 onEnterFrame: function() { 3781 if(this.isPlaying) { 3782 this.nextFrame(); 3783 } 3784 }, 3785 nextFrame: function() { 3786 this.setTime((this.getTimer() - this._startTime) / 1000); 3787 }, 3788 stop: function() { 3789 this.stopEnterFrame(); 3790 this.broadcastMessage('onStopped', { 3791 target: this, 3792 type: 'onStopped' 3793 }); 3794 }, 3795 stopEnterFrame: function() { 3796 this.isPlaying = false; 3797 }, 3798 continueTo: function(finish, duration) { 3799 this.begin = this._pos; 3800 this.setFinish(finish); 3801 if(this._duration != undefined) 3802 this.setDuration(duration); 3803 this.start(); 3804 }, 3805 resume: function() { 3806 this.fixTime(); 3807 this.startEnterFrame(); 3808 this.broadcastMessage('onResumed', { 3809 target: this, 3810 type: 'onResumed' 3811 }); 3812 }, 3813 yoyo: function() { 3814 this.continueTo(this.begin, this._time); 3815 }, 3816 addListener: function(o) { 3817 this.removeListener(o); 3818 return this._listeners.push(o); 3819 }, 3820 removeListener: function(o) { 3821 var a = this._listeners; 3822 var i = a.length; 3823 while(i--) { 3824 if(a[i] == o) { 3825 a.splice(i, 1); 3826 return true; 3827 } 3828 } 3829 return false; 3830 }, 3831 broadcastMessage: function() { 3832 var arr = []; 3833 for(var i = 0; i < arguments.length; i++) { 3834 arr.push(arguments[i]); 3835 } 3836 var e = arr.shift(); 3837 var a = this._listeners; 3838 var l = a.length; 3839 for(var i = 0; i < l; i++) { 3840 if(a[i][e]) { 3841 a[i][e].apply(a[i], arr); 3842 } 3843 } 3844 }, 3845 fixTime: function() { 3846 this._startTime = this.getTimer() - this._time * 1000; 3847 }, 3848 getTimer: function() { 3849 return new Date().getTime() - this._time; 3850 } 3851 }; 3852 3853 Kinetic.Tweens = { 3854 'back-ease-in': function(t, b, c, d, a, p) { 3855 var s = 1.70158; 3856 return c * (t /= d) * t * ((s + 1) * t - s) + b; 3857 }, 3858 'back-ease-out': function(t, b, c, d, a, p) { 3859 var s = 1.70158; 3860 return c * (( t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; 3861 }, 3862 'back-ease-in-out': function(t, b, c, d, a, p) { 3863 var s = 1.70158; 3864 if((t /= d / 2) < 1) { 3865 return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; 3866 } 3867 return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; 3868 }, 3869 'elastic-ease-in': function(t, b, c, d, a, p) { 3870 // added s = 0 3871 var s = 0; 3872 if(t === 0) { 3873 return b; 3874 } 3875 if((t /= d) == 1) { 3876 return b + c; 3877 } 3878 if(!p) { 3879 p = d * 0.3; 3880 } 3881 if(!a || a < Math.abs(c)) { 3882 a = c; 3883 s = p / 4; 3884 } 3885 else { 3886 s = p / (2 * Math.PI) * Math.asin(c / a); 3887 } 3888 return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; 3889 }, 3890 'elastic-ease-out': function(t, b, c, d, a, p) { 3891 // added s = 0 3892 var s = 0; 3893 if(t === 0) { 3894 return b; 3895 } 3896 if((t /= d) == 1) { 3897 return b + c; 3898 } 3899 if(!p) { 3900 p = d * 0.3; 3901 } 3902 if(!a || a < Math.abs(c)) { 3903 a = c; 3904 s = p / 4; 3905 } 3906 else { 3907 s = p / (2 * Math.PI) * Math.asin(c / a); 3908 } 3909 return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b); 3910 }, 3911 'elastic-ease-in-out': function(t, b, c, d, a, p) { 3912 // added s = 0 3913 var s = 0; 3914 if(t === 0) { 3915 return b; 3916 } 3917 if((t /= d / 2) == 2) { 3918 return b + c; 3919 } 3920 if(!p) { 3921 p = d * (0.3 * 1.5); 3922 } 3923 if(!a || a < Math.abs(c)) { 3924 a = c; 3925 s = p / 4; 3926 } 3927 else { 3928 s = p / (2 * Math.PI) * Math.asin(c / a); 3929 } 3930 if(t < 1) { 3931 return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; 3932 } 3933 return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b; 3934 }, 3935 'bounce-ease-out': function(t, b, c, d) { 3936 if((t /= d) < (1 / 2.75)) { 3937 return c * (7.5625 * t * t) + b; 3938 } 3939 else if(t < (2 / 2.75)) { 3940 return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; 3941 } 3942 else if(t < (2.5 / 2.75)) { 3943 return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; 3944 } 3945 else { 3946 return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; 3947 } 3948 }, 3949 'bounce-ease-in': function(t, b, c, d) { 3950 return c - Kinetic.Tweens['bounce-ease-out'](d - t, 0, c, d) + b; 3951 }, 3952 'bounce-ease-in-out': function(t, b, c, d) { 3953 if(t < d / 2) { 3954 return Kinetic.Tweens['bounce-ease-in'](t * 2, 0, c, d) * 0.5 + b; 3955 } 3956 else { 3957 return Kinetic.Tweens['bounce-ease-out'](t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; 3958 } 3959 }, 3960 // duplicate 3961 /* 3962 strongEaseInOut: function(t, b, c, d) { 3963 return c * (t /= d) * t * t * t * t + b; 3964 }, 3965 */ 3966 'ease-in': function(t, b, c, d) { 3967 return c * (t /= d) * t + b; 3968 }, 3969 'ease-out': function(t, b, c, d) { 3970 return -c * (t /= d) * (t - 2) + b; 3971 }, 3972 'ease-in-out': function(t, b, c, d) { 3973 if((t /= d / 2) < 1) { 3974 return c / 2 * t * t + b; 3975 } 3976 return -c / 2 * ((--t) * (t - 2) - 1) + b; 3977 }, 3978 'strong-ease-in': function(t, b, c, d) { 3979 return c * (t /= d) * t * t * t * t + b; 3980 }, 3981 'strong-ease-out': function(t, b, c, d) { 3982 return c * (( t = t / d - 1) * t * t * t * t + 1) + b; 3983 }, 3984 'strong-ease-in-out': function(t, b, c, d) { 3985 if((t /= d / 2) < 1) { 3986 return c / 2 * t * t * t * t * t + b; 3987 } 3988 return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; 3989 }, 3990 'linear': function(t, b, c, d) { 3991 return c * t / d + b; 3992 }, 3993 }; 3994