1 (function (global) { 2 3 "use strict"; 4 5 if (fabric.Canvas) { 6 fabric.warn('fabric.Canvas is already defined.'); 7 return; 8 } 9 10 var window = global.window, 11 document = window.document, 12 13 // aliases for faster resolution 14 extend = fabric.util.object.extend, 15 capitalize = fabric.util.string.capitalize, 16 camelize = fabric.util.string.camelize, 17 getPointer = fabric.util.getPointer, 18 getElementOffset = fabric.util.getElementOffset, 19 removeFromArray = fabric.util.removeFromArray, 20 addListener = fabric.util.addListener, 21 removeListener = fabric.util.removeListener, 22 23 utilMin = fabric.util.array.min, 24 utilMax = fabric.util.array.max, 25 26 sqrt = Math.sqrt, 27 pow = Math.pow, 28 atan2 = Math.atan2, 29 abs = Math.abs, 30 min = Math.min, 31 max = Math.max, 32 33 CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'), 34 FX_DURATION = 500, 35 STROKE_OFFSET = 0.5, 36 FX_TRANSITION = 'decel', 37 38 cursorMap = { 39 'tr': 'ne-resize', 40 'br': 'se-resize', 41 'bl': 'sw-resize', 42 'tl': 'nw-resize', 43 'ml': 'w-resize', 44 'mt': 'n-resize', 45 'mr': 'e-resize', 46 'mb': 's-resize' 47 }; 48 49 /** 50 * @class fabric.Canvas 51 * @constructor 52 * @param {HTMLElement | String} el <canvas> element to initialize instance on 53 * @param {Object} [options] Options object 54 */ 55 fabric.Canvas = function (el, options) { 56 57 options || (options = { }); 58 59 /** 60 * The object literal containing mouse position if clicked in an empty area (no image) 61 * @property _groupSelector 62 * @type object 63 */ 64 this._groupSelector = null; 65 66 /** 67 * The array literal containing all objects on canvas 68 * @property _objects 69 * @type array 70 */ 71 this._objects = []; 72 73 /** 74 * The element that references the canvas interface implementation 75 * @property _context 76 * @type object 77 */ 78 this._context = null; 79 80 /** 81 * The object literal containing the current x,y params of the transformation 82 * @property _currentTransform 83 * @type object 84 */ 85 this._currentTransform = null; 86 87 /** 88 * References instance of fabric.Group - when multiple objects are selected 89 * @property _activeGroup 90 * @type object 91 */ 92 this._activeGroup = null; 93 94 /** 95 * X coordinates of a path, captured during free drawing 96 */ 97 this._freeDrawingXPoints = [ ]; 98 99 /** 100 * Y coordinates of a path, captured during free drawing 101 */ 102 this._freeDrawingYPoints = [ ]; 103 104 this._createUpperCanvas(el); 105 this._initOptions(options); 106 this._initWrapperElement(); 107 this._createLowerCanvas(); 108 109 this._initEvents(); 110 111 if (options.overlayImage) { 112 this.setOverlayImage(options.overlayImage); 113 } 114 115 this.calcOffset(); 116 117 fabric.Canvas.activeInstance = this; 118 }; 119 120 extend(fabric.Canvas.prototype, fabric.Observable); 121 122 extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ { 123 124 /** 125 * Background color of this canvas instance 126 * @property 127 * @type String 128 */ 129 backgroundColor: 'rgba(0, 0, 0, 0)', 130 131 /** 132 * Indicates whether object selection should be enabled 133 * @property 134 * @type Boolean 135 */ 136 selection: true, 137 138 /** 139 * Color of selection 140 * @property 141 * @type String 142 */ 143 selectionColor: 'rgba(100, 100, 255, 0.3)', // blue 144 145 /** 146 * Color of the border of selection (usually slightly darker than color of selection itself) 147 * @property 148 * @type String 149 */ 150 selectionBorderColor: 'rgba(255, 255, 255, 0.3)', 151 152 /** 153 * Width of a line used in selection 154 * @property 155 * @type Number 156 */ 157 selectionLineWidth: 1, 158 159 /** 160 * Color of the line used in free drawing mode 161 * @property 162 * @type String 163 */ 164 freeDrawingColor: 'rgb(0, 0, 0)', 165 166 /** 167 * Width of a line used in free drawing mode 168 * @property 169 * @type Number 170 */ 171 freeDrawingLineWidth: 1, 172 173 /** 174 * @property 175 * @type Boolean 176 */ 177 includeDefaultValues: true, 178 179 /** 180 * Indicates whether images loaded via `fabric.Canvas#loadImageFromUrl` should be cached 181 * @property 182 * @type Boolean 183 */ 184 shouldCacheImages: false, 185 186 /** 187 * Indicates whether objects' state should be saved 188 * @property 189 * @type Boolean 190 */ 191 stateful: true, 192 193 /** 194 * Indicates whether fabric.Canvas#add should also re-render canvas. 195 * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once 196 * (followed by a manual rendering after addition) 197 */ 198 renderOnAddition: true, 199 200 /** 201 * @constant 202 * @type Number 203 */ 204 CANVAS_WIDTH: 600, 205 206 /** 207 * @constant 208 * @type Number 209 */ 210 CANVAS_HEIGHT: 600, 211 212 /** 213 * @constant 214 * @type String 215 */ 216 CONTAINER_CLASS: 'canvas-container', 217 218 /** 219 * Callback; invoked right before object is about to be scaled/rotated 220 * @method onBeforeScaleRotate 221 * @param {fabric.Object} target Object that's about to be scaled/rotated 222 */ 223 onBeforeScaleRotate: function (target) { 224 /* NOOP */ 225 }, 226 227 /** 228 * Callback; invoked on every redraw of canvas and is being passed a number indicating current fps 229 * @method onFpsUpdate 230 * @param {Number} fps 231 */ 232 onFpsUpdate: null, 233 234 /** 235 * Calculates canvas element offset relative to the document 236 * This method is also attached as "resize" event handler of window 237 * @method calcOffset 238 * @return {fabric.Canvas} instance 239 * @chainable 240 */ 241 calcOffset: function () { 242 this._offset = getElementOffset(this.upperCanvasEl); 243 return this; 244 }, 245 246 /** 247 * Sets overlay image for this canvas 248 * @method setOverlayImage 249 * @param {String} url url of an image to set background to 250 * @param {Function} callback callback to invoke when image is loaded and set as an overlay one 251 * @return {fabric.Canvas} thisArg 252 * @chainable 253 */ 254 setOverlayImage: function (url, callback) { // TODO (kangax): test callback 255 if (url) { 256 var _this = this, img = new Image(); 257 258 /** @ignore */ 259 img.onload = function () { 260 _this.overlayImage = img; 261 if (callback) { 262 callback(); 263 } 264 img = img.onload = null; 265 }; 266 img.src = url; 267 } 268 return this; 269 }, 270 271 /** 272 * @private 273 * @method _initWrapperElement 274 * @param {Number} width 275 * @param {Number} height 276 */ 277 _initWrapperElement: function () { 278 this.wrapperEl = fabric.util.wrapElement(this.upperCanvasEl, 'div', { 279 'class': this.CONTAINER_CLASS 280 }); 281 fabric.util.setStyle(this.wrapperEl, { 282 width: this.getWidth() + 'px', 283 height: this.getHeight() + 'px', 284 position: 'relative' 285 }); 286 fabric.util.makeElementUnselectable(this.wrapperEl); 287 }, 288 289 /** 290 * @private 291 * @method _applyCanvasStyle 292 * @param {Element} element 293 */ 294 _applyCanvasStyle: function (element) { 295 var width = this.getWidth() || element.width, 296 height = this.getHeight() || element.height; 297 298 fabric.util.setStyle(element, { 299 position: 'absolute', 300 width: width + 'px', 301 height: height + 'px', 302 left: 0, 303 top: 0 304 }); 305 element.width = width; 306 element.height = height; 307 fabric.util.makeElementUnselectable(element); 308 }, 309 310 /** 311 * @private 312 * @method _createCanvasElement 313 * @param {Element} element 314 */ 315 _createCanvasElement: function() { 316 var element = document.createElement('canvas'); 317 if (!element) { 318 throw CANVAS_INIT_ERROR; 319 } 320 this._initCanvasElement(element); 321 return element; 322 }, 323 324 _initCanvasElement: function(element) { 325 if (typeof element.getContext === 'undefined' && 326 typeof G_vmlCanvasManager !== 'undefined' && 327 G_vmlCanvasManager.initElement) { 328 329 G_vmlCanvasManager.initElement(element); 330 } 331 if (typeof element.getContext === 'undefined') { 332 throw CANVAS_INIT_ERROR; 333 } 334 }, 335 336 /** 337 * @method _initOptions 338 * @param {Object} options 339 */ 340 _initOptions: function (options) { 341 for (var prop in options) { 342 this[prop] = options[prop]; 343 } 344 345 this.width = parseInt(this.upperCanvasEl.width, 10) || 0; 346 this.height = parseInt(this.upperCanvasEl.height, 10) || 0; 347 348 this.upperCanvasEl.style.width = this.width + 'px'; 349 this.upperCanvasEl.style.height = this.height + 'px'; 350 }, 351 352 /** 353 * Adds mouse listeners to canvas 354 * @method _initEvents 355 * @private 356 * See configuration documentation for more details. 357 */ 358 _initEvents: function () { 359 360 var _this = this; 361 362 this._onMouseDown = function (e) { 363 _this.__onMouseDown(e); 364 addListener(document, 'mouseup', _this._onMouseUp); 365 }; 366 this._onMouseUp = function (e) { 367 _this.__onMouseUp(e); 368 removeListener(document, 'mouseup', _this._onMouseUp); 369 }; 370 this._onMouseMove = function (e) { _this.__onMouseMove(e); }; 371 this._onResize = function (e) { _this.calcOffset() }; 372 373 addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); 374 addListener(document, 'mousemove', this._onMouseMove); 375 addListener(window, 'resize', this._onResize); 376 }, 377 378 /** 379 * @method _createUpperCanvas 380 * @param {HTMLElement|String} canvasEl Canvas element 381 * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized 382 */ 383 _createUpperCanvas: function (canvasEl) { 384 this.upperCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); 385 this._initCanvasElement(this.upperCanvasEl); 386 387 fabric.util.addClass(this.upperCanvasEl, 'upper-canvas'); 388 this._applyCanvasStyle(this.upperCanvasEl); 389 390 this.contextTop = this.upperCanvasEl.getContext('2d'); 391 }, 392 393 /** 394 * Creates a secondary canvas 395 * @method _createLowerCanvas 396 */ 397 _createLowerCanvas: function () { 398 this.lowerCanvasEl = this._createCanvasElement(); 399 this.lowerCanvasEl.className = 'lower-canvas'; 400 401 this.wrapperEl.insertBefore(this.lowerCanvasEl, this.upperCanvasEl); 402 403 this._applyCanvasStyle(this.lowerCanvasEl); 404 this.contextContainer = this.lowerCanvasEl.getContext('2d'); 405 }, 406 407 /** 408 * Returns canvas width 409 * @method getWidth 410 * @return {Number} 411 */ 412 getWidth: function () { 413 return this.width; 414 }, 415 416 /** 417 * Returns canvas height 418 * @method getHeight 419 * @return {Number} 420 */ 421 getHeight: function () { 422 return this.height; 423 }, 424 425 /** 426 * Sets width of this canvas instance 427 * @method setWidth 428 * @param {Number} width value to set width to 429 * @return {fabric.Canvas} instance 430 * @chainable true 431 */ 432 setWidth: function (value) { 433 return this._setDimension('width', value); 434 }, 435 436 /** 437 * Sets height of this canvas instance 438 * @method setHeight 439 * @param {Number} height value to set height to 440 * @return {fabric.Canvas} instance 441 * @chainable true 442 */ 443 setHeight: function (value) { 444 return this._setDimension('height', value); 445 }, 446 447 /** 448 * Sets dimensions (width, height) of this canvas instance 449 * @method setDimensions 450 * @param {Object} dimensions 451 * @return {fabric.Canvas} thisArg 452 * @chainable 453 */ 454 setDimensions: function(dimensions) { 455 for (var prop in dimensions) { 456 this._setDimension(prop, dimensions[prop]); 457 } 458 return this; 459 }, 460 461 /** 462 * Helper for setting width/height 463 * @private 464 * @method _setDimensions 465 * @param {String} prop property (width|height) 466 * @param {Number} value value to set property to 467 * @return {fabric.Canvas} instance 468 * @chainable true 469 */ 470 _setDimension: function (prop, value) { 471 this.lowerCanvasEl[prop] = value; 472 this.lowerCanvasEl.style[prop] = value + 'px'; 473 474 this.upperCanvasEl[prop] = value; 475 this.upperCanvasEl.style[prop] = value + 'px'; 476 477 this.wrapperEl.style[prop] = value + 'px'; 478 479 this[prop] = value; 480 481 this.calcOffset(); 482 this.renderAll(); 483 484 return this; 485 }, 486 487 /** 488 * Method that defines the actions when mouse is released on canvas. 489 * The method resets the currentTransform parameters, store the image corner 490 * position in the image object and render the canvas on top. 491 * @method __onMouseUp 492 * @param {Event} e Event object fired on mouseup 493 * 494 */ 495 __onMouseUp: function (e) { 496 497 if (this.isDrawingMode && this._isCurrentlyDrawing) { 498 this._finalizeDrawingPath(); 499 return; 500 } 501 502 if (this._currentTransform) { 503 504 var transform = this._currentTransform, 505 target = transform.target; 506 507 if (target._scaling) { 508 this.fire('object:scaled', { target: target }); 509 target._scaling = false; 510 } 511 512 // determine the new coords everytime the image changes its position 513 var i = this._objects.length; 514 while (i--) { 515 this._objects[i].setCoords(); 516 } 517 518 // only fire :modified event if target coordinates were changed during mousedown-mouseup 519 if (this.stateful && target.hasStateChanged()) { 520 target.isMoving = false; 521 this.fire('object:modified', { target: target }); 522 } 523 } 524 525 this._currentTransform = null; 526 527 if (this._groupSelector) { 528 // group selection was completed, determine its bounds 529 this._findSelectedObjects(e); 530 } 531 var activeGroup = this.getActiveGroup(); 532 if (activeGroup) { 533 if (this.stateful && activeGroup.hasStateChanged() && 534 activeGroup.containsPoint(this.getPointer(e))) { 535 this.fire('group:modified', { target: activeGroup }); 536 } 537 activeGroup.setObjectsCoords(); 538 activeGroup.set('isMoving', false); 539 this._setCursor('default'); 540 } 541 542 // clear selection 543 this._groupSelector = null; 544 this.renderAll(); 545 546 this._setCursorFromEvent(e, target); 547 548 // fix for FF 549 this._setCursor(''); 550 551 var _this = this; 552 setTimeout(function () { 553 _this._setCursorFromEvent(e, target); 554 }, 50); 555 556 this.fire('mouse:up'); 557 }, 558 559 _shouldClearSelection: function (e) { 560 var target = this.findTarget(e), 561 activeGroup = this.getActiveGroup(); 562 return ( 563 !target || ( 564 target && 565 activeGroup && 566 !activeGroup.contains(target) && 567 activeGroup !== target && 568 !e.shiftKey 569 ) 570 ); 571 }, 572 573 /** 574 * Method that defines the actions when mouse is clic ked on canvas. 575 * The method inits the currentTransform parameters and renders all the 576 * canvas so the current image can be placed on the top canvas and the rest 577 * in on the container one. 578 * @method __onMouseDown 579 * @param e {Event} Event object fired on mousedown 580 * 581 */ 582 __onMouseDown: function (e) { 583 584 // accept only left clicks 585 if (e.which !== 1) return; 586 587 if (this.isDrawingMode) { 588 this._prepareForDrawing(e); 589 590 // capture coordinates immediately; this allows to draw dots (when movement never occurs) 591 this._captureDrawingPath(e); 592 593 return; 594 } 595 596 // ignore if some object is being transformed at this moment 597 if (this._currentTransform) return; 598 599 var target = this.findTarget(e), 600 pointer = this.getPointer(e), 601 activeGroup = this.getActiveGroup(), 602 corner; 603 604 if (this._shouldClearSelection(e)) { 605 606 this._groupSelector = { 607 ex: pointer.x, 608 ey: pointer.y, 609 top: 0, 610 left: 0 611 }; 612 613 this.deactivateAllWithDispatch(); 614 } 615 else { 616 // determine if it's a drag or rotate case 617 // rotate and scale will happen at the same time 618 this.stateful && target.saveState(); 619 620 if (corner = target._findTargetCorner(e, this._offset)) { 621 this.onBeforeScaleRotate(target); 622 } 623 624 this._setupCurrentTransform(e, target); 625 626 var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()); 627 if (shouldHandleGroupLogic) { 628 this._handleGroupLogic(e, target); 629 } 630 else { 631 if (target !== this.getActiveGroup()) { 632 this.deactivateAll(); 633 } 634 this.setActiveObject(target); 635 } 636 } 637 // we must renderAll so that active image is placed on the top canvas 638 this.renderAll(); 639 640 this.fire('mouse:down'); 641 }, 642 643 /** 644 * Returns <canvas> element corresponding to this instance 645 * @method getElement 646 * @return {HTMLCanvasElement} 647 */ 648 getElement: function () { 649 return this.upperCanvasEl; 650 }, 651 652 /** 653 * Deactivates all objects and dispatches appropriate events 654 * @method deactivateAllWithDispatch 655 * @return {fabric.Canvas} thisArg 656 */ 657 deactivateAllWithDispatch: function () { 658 var activeGroup = this.getActiveGroup(); 659 if (activeGroup) { 660 this.fire('before:group:destroyed', { 661 target: activeGroup 662 }); 663 } 664 this.deactivateAll(); 665 if (activeGroup) { 666 this.fire('after:group:destroyed'); 667 } 668 this.fire('selection:cleared'); 669 return this; 670 }, 671 672 /** 673 * @private 674 * @method _setupCurrentTransform 675 */ 676 _setupCurrentTransform: function (e, target) { 677 var action = 'drag', 678 corner, 679 pointer = getPointer(e); 680 681 if (corner = target._findTargetCorner(e, this._offset)) { 682 action = (corner === 'ml' || corner === 'mr') 683 ? 'scaleX' 684 : (corner === 'mt' || corner === 'mb') 685 ? 'scaleY' 686 : 'rotate'; 687 } 688 689 this._currentTransform = { 690 target: target, 691 action: action, 692 scaleX: target.scaleX, 693 scaleY: target.scaleY, 694 offsetX: pointer.x - target.left, 695 offsetY: pointer.y - target.top, 696 ex: pointer.x, 697 ey: pointer.y, 698 left: target.left, 699 top: target.top, 700 theta: target.theta, 701 width: target.width * target.scaleX 702 }; 703 704 this._currentTransform.original = { 705 left: target.left, 706 top: target.top 707 }; 708 }, 709 710 _handleGroupLogic: function (e, target) { 711 if (target.isType('group')) { 712 // if it's a group, find target again, this time skipping group 713 target = this.findTarget(e, true); 714 // if even object is not found, bail out 715 if (!target || target.isType('group')) { 716 return; 717 } 718 } 719 var activeGroup = this.getActiveGroup(); 720 if (activeGroup) { 721 if (activeGroup.contains(target)) { 722 activeGroup.remove(target); 723 target.setActive(false); 724 if (activeGroup.size() === 1) { 725 // remove group alltogether if after removal it only contains 1 object 726 this.removeActiveGroup(); 727 } 728 } 729 else { 730 activeGroup.add(target); 731 } 732 this.fire('group:selected', { target: activeGroup }); 733 activeGroup.setActive(true); 734 } 735 else { 736 // group does not exist 737 if (this._activeObject) { 738 // only if there's an active object 739 if (target !== this._activeObject) { 740 // and that object is not the actual target 741 var group = new fabric.Group([ this._activeObject,target ]); 742 this.setActiveGroup(group); 743 activeGroup = this.getActiveGroup(); 744 } 745 } 746 // activate target object in any case 747 target.setActive(true); 748 } 749 750 if (activeGroup) { 751 activeGroup.saveCoords(); 752 } 753 }, 754 755 /** 756 * @private 757 * @method _prepareForDrawing 758 */ 759 _prepareForDrawing: function(e) { 760 761 this._isCurrentlyDrawing = true; 762 763 this.removeActiveObject().renderAll(); 764 765 var pointer = this.getPointer(e); 766 767 this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; 768 769 this._freeDrawingXPoints.push(pointer.x); 770 this._freeDrawingYPoints.push(pointer.y); 771 772 this.contextTop.beginPath(); 773 this.contextTop.moveTo(pointer.x, pointer.y); 774 this.contextTop.strokeStyle = this.freeDrawingColor; 775 this.contextTop.lineWidth = this.freeDrawingLineWidth; 776 this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; 777 }, 778 779 /** 780 * @private 781 * @method _captureDrawingPath 782 */ 783 _captureDrawingPath: function(e) { 784 var pointer = this.getPointer(e); 785 786 this._freeDrawingXPoints.push(pointer.x); 787 this._freeDrawingYPoints.push(pointer.y); 788 789 this.contextTop.lineTo(pointer.x, pointer.y); 790 this.contextTop.stroke(); 791 }, 792 793 /** 794 * @private 795 * @method _finalizeDrawingPath 796 */ 797 _finalizeDrawingPath: function() { 798 799 this.contextTop.closePath(); 800 801 this._isCurrentlyDrawing = false; 802 803 var minX = utilMin(this._freeDrawingXPoints), 804 minY = utilMin(this._freeDrawingYPoints), 805 maxX = utilMax(this._freeDrawingXPoints), 806 maxY = utilMax(this._freeDrawingYPoints), 807 ctx = this.contextTop, 808 path = [ ], 809 xPoint, 810 yPoint, 811 xPoints = this._freeDrawingXPoints, 812 yPoints = this._freeDrawingYPoints; 813 814 path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); 815 816 for (var i = 1; xPoint = xPoints[i], yPoint = yPoints[i]; i++) { 817 path.push('L ', xPoint - minX, ' ', yPoint - minY, ' '); 818 } 819 820 // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, 821 // and instead fire something like "drawing:completed" event with path string 822 823 path = path.join(''); 824 825 if (path === "M 0 0 L 0 0 ") { 826 // do not create 0 width/height paths, as they are rendered inconsistently across browsers 827 // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing 828 return; 829 } 830 831 var p = new fabric.Path(path); 832 833 p.fill = null; 834 p.stroke = this.freeDrawingColor; 835 p.strokeWidth = this.freeDrawingLineWidth; 836 this.add(p); 837 p.set("left", minX + (maxX - minX) / 2).set("top", minY + (maxY - minY) / 2).setCoords(); 838 this.renderAll(); 839 this.fire('path:created', { path: p }); 840 }, 841 842 /** 843 * Method that defines the actions when mouse is hovering the canvas. 844 * The currentTransform parameter will definde whether the user is rotating/scaling/translating 845 * an image or neither of them (only hovering). A group selection is also possible and would cancel 846 * all any other type of action. 847 * In case of an image transformation only the top canvas will be rendered. 848 * @method __onMouseMove 849 * @param e {Event} Event object fired on mousemove 850 * 851 */ 852 __onMouseMove: function (e) { 853 854 if (this.isDrawingMode) { 855 if (this._isCurrentlyDrawing) { 856 this._captureDrawingPath(e); 857 } 858 return; 859 } 860 861 var groupSelector = this._groupSelector; 862 863 // We initially clicked in an empty area, so we draw a box for multiple selection. 864 if (groupSelector !== null) { 865 var pointer = getPointer(e); 866 groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; 867 groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; 868 this.renderTop(); 869 } 870 else if (!this._currentTransform) { 871 872 // alias style to elimintate unnecessary lookup 873 var style = this.upperCanvasEl.style; 874 875 // Here we are hovering the canvas then we will determine 876 // what part of the pictures we are hovering to change the caret symbol. 877 // We won't do that while dragging or rotating in order to improve the 878 // performance. 879 var target = this.findTarget(e); 880 881 if (!target) { 882 // image/text was hovered-out from, we remove its borders 883 for (var i = this._objects.length; i--; ) { 884 if (!this._objects[i].active) { 885 this._objects[i].setActive(false); 886 } 887 } 888 style.cursor = 'default'; 889 } 890 else { 891 // set proper cursor 892 this._setCursorFromEvent(e, target); 893 if (target.isActive()) { 894 // display corners when hovering over an image 895 target.setCornersVisibility && target.setCornersVisibility(true); 896 } 897 } 898 } 899 else { 900 // object is being transformed (scaled/rotated/moved/etc.) 901 var pointer = getPointer(e), 902 x = pointer.x, 903 y = pointer.y; 904 905 this._currentTransform.target.isMoving = true; 906 907 if (this._currentTransform.action === 'rotate') { 908 // rotate object only if shift key is not pressed 909 // and if it is not a group we are transforming 910 911 if (!e.shiftKey) { 912 this._rotateObject(x, y); 913 } 914 this._scaleObject(x, y); 915 } 916 else if (this._currentTransform.action === 'scaleX') { 917 this._scaleObject(x, y, 'x'); 918 } 919 else if (this._currentTransform.action === 'scaleY') { 920 this._scaleObject(x, y, 'y'); 921 } 922 else { 923 this._translateObject(x, y); 924 925 this.fire('object:moved', { 926 target: this._currentTransform.target 927 }); 928 } 929 // only commit here. when we are actually moving the pictures 930 this.renderAll(); 931 } 932 }, 933 934 /** 935 * Translates object by "setting" its left/top 936 * @method _translateObject 937 * @param x {Number} pointer's x coordinate 938 * @param y {Number} pointer's y coordinate 939 */ 940 _translateObject: function (x, y) { 941 var target = this._currentTransform.target; 942 target.lockMovementX || target.set('left', x - this._currentTransform.offsetX); 943 target.lockMovementY || target.set('top', y - this._currentTransform.offsetY); 944 }, 945 946 /** 947 * Scales object by invoking its scaleX/scaleY methods 948 * @method _scaleObject 949 * @param x {Number} pointer's x coordinate 950 * @param y {Number} pointer's y coordinate 951 * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. 952 * When not provided, an object is scaled by both dimensions equally 953 */ 954 _scaleObject: function (x, y, by) { 955 var t = this._currentTransform, 956 offset = this._offset, 957 target = t.target; 958 959 if (target.lockScalingX && target.lockScalingY) return; 960 961 var lastLen = sqrt(pow(t.ey - t.top - offset.top, 2) + pow(t.ex - t.left - offset.left, 2)), 962 curLen = sqrt(pow(y - t.top - offset.top, 2) + pow(x - t.left - offset.left, 2)); 963 964 target._scaling = true; 965 966 if (!by) { 967 target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); 968 target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); 969 } 970 else if (by === 'x' && !target.lockUniScaling) { 971 target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); 972 } 973 else if (by === 'y' && !target.lockUniScaling) { 974 target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); 975 } 976 }, 977 978 /** 979 * Rotates object by invoking its rotate method 980 * @method _rotateObject 981 * @param x {Number} pointer's x coordinate 982 * @param y {Number} pointer's y coordinate 983 */ 984 _rotateObject: function (x, y) { 985 986 var t = this._currentTransform, 987 o = this._offset; 988 989 if (t.target.lockRotation) return; 990 991 var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left), 992 curAngle = atan2(y - t.top - o.top, x - t.left - o.left); 993 994 t.target.set('theta', (curAngle - lastAngle) + t.theta); 995 }, 996 997 /** 998 * @method _setCursor 999 */ 1000 _setCursor: function (value) { 1001 this.upperCanvasEl.style.cursor = value; 1002 }, 1003 1004 /** 1005 * Sets the cursor depending on where the canvas is being hovered. 1006 * Note: very buggy in Opera 1007 * @method _setCursorFromEvent 1008 * @param e {Event} Event object 1009 * @param target {Object} Object that the mouse is hovering, if so. 1010 */ 1011 _setCursorFromEvent: function (e, target) { 1012 var s = this.upperCanvasEl.style; 1013 if (!target) { 1014 s.cursor = 'default'; 1015 return false; 1016 } 1017 else { 1018 var activeGroup = this.getActiveGroup(); 1019 // only show proper corner when group selection is not active 1020 var corner = !!target._findTargetCorner 1021 && (!activeGroup || !activeGroup.contains(target)) 1022 && target._findTargetCorner(e, this._offset); 1023 1024 if (!corner) { 1025 s.cursor = 'move'; 1026 } 1027 else { 1028 if (corner in cursorMap) { 1029 s.cursor = cursorMap[corner]; 1030 } 1031 else { 1032 s.cursor = 'default'; 1033 return false; 1034 } 1035 } 1036 } 1037 return true; 1038 }, 1039 1040 /** 1041 * Given a context, renders an object on that context 1042 * @param ctx {Object} context to render object on 1043 * @param object {Object} object to render 1044 * @private 1045 */ 1046 _draw: function (ctx, object) { 1047 object && object.render(ctx); 1048 }, 1049 1050 /** 1051 * @method _drawSelection 1052 * @private 1053 */ 1054 _drawSelection: function () { 1055 var groupSelector = this._groupSelector, 1056 left = groupSelector.left, 1057 top = groupSelector.top, 1058 aleft = abs(left), 1059 atop = abs(top); 1060 1061 this.contextTop.fillStyle = this.selectionColor; 1062 1063 this.contextTop.fillRect( 1064 groupSelector.ex - ((left > 0) ? 0 : -left), 1065 groupSelector.ey - ((top > 0) ? 0 : -top), 1066 aleft, 1067 atop 1068 ); 1069 1070 this.contextTop.lineWidth = this.selectionLineWidth; 1071 this.contextTop.strokeStyle = this.selectionBorderColor; 1072 1073 this.contextTop.strokeRect( 1074 groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), 1075 groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), 1076 aleft, 1077 atop 1078 ); 1079 }, 1080 1081 _findSelectedObjects: function (e) { 1082 var target, 1083 targetRegion, 1084 group = [ ], 1085 x1 = this._groupSelector.ex, 1086 y1 = this._groupSelector.ey, 1087 x2 = x1 + this._groupSelector.left, 1088 y2 = y1 + this._groupSelector.top, 1089 currentObject, 1090 selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), 1091 selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)); 1092 1093 for (var i = 0, len = this._objects.length; i < len; ++i) { 1094 currentObject = this._objects[i]; 1095 1096 if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || 1097 currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { 1098 1099 if (this.selection && currentObject.selectable) { 1100 currentObject.setActive(true); 1101 group.push(currentObject); 1102 } 1103 } 1104 } 1105 1106 // do not create group for 1 element only 1107 if (group.length === 1) { 1108 this.setActiveObject(group[0]); 1109 this.fire('object:selected', { 1110 target: group[0] 1111 }); 1112 } 1113 else if (group.length > 1) { 1114 var group = new fabric.Group(group); 1115 this.setActiveGroup(group); 1116 group.saveCoords(); 1117 this.fire('group:selected', { target: group }); 1118 } 1119 1120 this.renderAll(); 1121 }, 1122 1123 /** 1124 * Adds objects to canvas, then renders canvas; 1125 * Objects should be instances of (or inherit from) fabric.Object 1126 * @method add 1127 * @return {fabric.Canvas} thisArg 1128 * @chainable 1129 */ 1130 add: function () { 1131 this._objects.push.apply(this._objects, arguments); 1132 for (var i = arguments.length; i--; ) { 1133 this.stateful && arguments[i].setupState(); 1134 arguments[i].setCoords(); 1135 } 1136 this.renderOnAddition && this.renderAll(); 1137 return this; 1138 }, 1139 1140 /** 1141 * Inserts an object to canvas at specified index and renders canvas. 1142 * An object should be an instance of (or inherit from) fabric.Object 1143 * @method insertAt 1144 * @param object {Object} Object to insert 1145 * @param index {Number} index to insert object at 1146 * @return {fabric.Canvas} instance 1147 */ 1148 insertAt: function (object, index) { 1149 this._objects.splice(index, 0, object); 1150 this.stateful && object.setupState(); 1151 object.setCoords(); 1152 this.renderAll(); 1153 return this; 1154 }, 1155 1156 /** 1157 * Returns an array of objects this instance has 1158 * @method getObjects 1159 * @return {Array} 1160 */ 1161 getObjects: function () { 1162 return this._objects; 1163 }, 1164 1165 /** 1166 * Returns topmost canvas context 1167 * @method getContext 1168 * @return {CanvasRenderingContext2D} 1169 */ 1170 getContext: function () { 1171 return this.contextTop; 1172 }, 1173 1174 /** 1175 * Clears specified context of canvas element 1176 * @method clearContext 1177 * @param context {Object} ctx context to clear 1178 * @return {fabric.Canvas} thisArg 1179 * @chainable 1180 */ 1181 clearContext: function(ctx) { 1182 ctx.clearRect(0, 0, this.width, this.height); 1183 return this; 1184 }, 1185 1186 /** 1187 * Clears all contexts (background, main, top) of an instance 1188 * @method clear 1189 * @return {fabric.Canvas} thisArg 1190 * @chainable 1191 */ 1192 clear: function () { 1193 this._objects.length = 0; 1194 this.clearContext(this.contextTop); 1195 this.clearContext(this.contextContainer); 1196 this.renderAll(); 1197 return this; 1198 }, 1199 1200 /** 1201 * Renders both the top canvas and the secondary container canvas. 1202 * @method renderAll 1203 * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas 1204 * @return {fabric.Canvas} instance 1205 * @chainable 1206 */ 1207 renderAll: function (allOnTop) { 1208 1209 var containerCanvas = this[allOnTop ? 'contextTop' : 'contextContainer']; 1210 1211 this.clearContext(this.contextTop); 1212 1213 if (!allOnTop) { 1214 this.clearContext(containerCanvas); 1215 } 1216 1217 var length = this._objects.length, 1218 activeGroup = this.getActiveGroup(), 1219 startTime = new Date(); 1220 1221 if (this.clipTo) { 1222 containerCanvas.save(); 1223 containerCanvas.beginPath(); 1224 this.clipTo(containerCanvas); 1225 containerCanvas.clip(); 1226 } 1227 1228 containerCanvas.fillStyle = this.backgroundColor; 1229 containerCanvas.fillRect(0, 0, this.width, this.height); 1230 1231 if (length) { 1232 for (var i = 0; i < length; ++i) { 1233 if (!activeGroup || 1234 (activeGroup && 1235 !activeGroup.contains(this._objects[i]))) { 1236 this._draw(containerCanvas, this._objects[i]); 1237 } 1238 } 1239 } 1240 1241 if (this.clipTo) { 1242 containerCanvas.restore(); 1243 } 1244 1245 // delegate rendering to group selection (if one exists) 1246 if (activeGroup) { 1247 this._draw(this.contextTop, activeGroup); 1248 } 1249 1250 if (this.overlayImage) { 1251 this.contextTop.drawImage(this.overlayImage, 0, 0); 1252 } 1253 1254 if (this.onFpsUpdate) { 1255 var elapsedTime = new Date() - startTime; 1256 this.onFpsUpdate(~~(1000 / elapsedTime)); 1257 } 1258 1259 this.fire('after:render'); 1260 1261 return this; 1262 }, 1263 1264 /** 1265 * Method to render only the top canvas. 1266 * Also used to render the group selection box. 1267 * @method renderTop 1268 * @return {fabric.Canvas} thisArg 1269 * @chainable 1270 */ 1271 renderTop: function () { 1272 1273 this.clearContext(this.contextTop); 1274 if (this.overlayImage) { 1275 this.contextTop.drawImage(this.overlayImage, 0, 0); 1276 } 1277 1278 // we render the top context - last object 1279 if (this.selection && this._groupSelector) { 1280 this._drawSelection(); 1281 } 1282 1283 // delegate rendering to group selection if one exists 1284 // used for drawing selection borders/corners 1285 var activeGroup = this.getActiveGroup(); 1286 if (activeGroup) { 1287 activeGroup.render(this.contextTop); 1288 } 1289 1290 this.fire('after:render'); 1291 1292 return this; 1293 }, 1294 1295 /** 1296 * Applies one implementation of 'point inside polygon' algorithm 1297 * @method containsPoint 1298 * @param e { Event } event object 1299 * @param target { fabric.Object } object to test against 1300 * @return {Boolean} true if point contains within area of given object 1301 */ 1302 containsPoint: function (e, target) { 1303 var pointer = this.getPointer(e), 1304 xy = this._normalizePointer(target, pointer), 1305 x = xy.x, 1306 y = xy.y; 1307 1308 // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html 1309 // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html 1310 1311 // we iterate through each object. If target found, return it. 1312 var iLines = target._getImageLines(target.oCoords), 1313 xpoints = target._findCrossPoints(x, y, iLines); 1314 1315 // if xcount is odd then we clicked inside the object 1316 // For the specific case of square images xcount === 1 in all true cases 1317 if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { 1318 return true; 1319 } 1320 return false; 1321 }, 1322 1323 /** 1324 * @private 1325 * @method _normalizePointer 1326 */ 1327 _normalizePointer: function (object, pointer) { 1328 1329 var activeGroup = this.getActiveGroup(), 1330 x = pointer.x, 1331 y = pointer.y; 1332 1333 var isObjectInGroup = ( 1334 activeGroup && 1335 object.type !== 'group' && 1336 activeGroup.contains(object) 1337 ); 1338 1339 if (isObjectInGroup) { 1340 x -= activeGroup.left; 1341 y -= activeGroup.top; 1342 } 1343 return { x: x, y: y }; 1344 }, 1345 1346 /** 1347 * Method that determines what object we are clicking on 1348 * @method findTarget 1349 * @param {Event} e mouse event 1350 * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through 1351 */ 1352 findTarget: function (e, skipGroup) { 1353 1354 var target, 1355 pointer = this.getPointer(e); 1356 1357 // first check current group (if one exists) 1358 var activeGroup = this.getActiveGroup(); 1359 1360 if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { 1361 target = activeGroup; 1362 return target; 1363 } 1364 1365 // then check all of the objects on canvas 1366 for (var i = this._objects.length; i--; ) { 1367 if (this.containsPoint(e, this._objects[i])) { 1368 target = this._objects[i]; 1369 this.relatedTarget = target; 1370 break; 1371 } 1372 } 1373 if (this.selection && target && target.selectable) { 1374 return target; 1375 } 1376 }, 1377 1378 /** 1379 * Exports canvas element to a dataurl image. 1380 * @method toDataURL 1381 * @param {String} format the format of the output image. Either "jpeg" or "png". 1382 * @return {String} 1383 */ 1384 toDataURL: function (format) { 1385 var data; 1386 if (!format) { 1387 format = 'png'; 1388 } 1389 if (format === 'jpeg' || format === 'png') { 1390 this.renderAll(true); 1391 data = this.upperCanvasEl.toDataURL('image/' + format); 1392 this.renderAll(); 1393 } 1394 return data; 1395 }, 1396 1397 /** 1398 * Exports canvas element to a dataurl image (allowing to change image size via multiplier). 1399 * @method toDataURLWithMultiplier 1400 * @param {String} format (png|jpeg) 1401 * @param {Number} multiplier 1402 * @return {String} 1403 */ 1404 toDataURLWithMultiplier: function (format, multiplier) { 1405 1406 var origWidth = this.getWidth(), 1407 origHeight = this.getHeight(), 1408 scaledWidth = origWidth * multiplier, 1409 scaledHeight = origHeight * multiplier, 1410 activeObject = this.getActiveObject(); 1411 1412 this.setWidth(scaledWidth).setHeight(scaledHeight); 1413 this.contextTop.scale(multiplier, multiplier); 1414 1415 if (activeObject) { 1416 this.deactivateAll().renderAll(); 1417 } 1418 var dataURL = this.toDataURL(format); 1419 1420 this.contextTop.scale( 1 / multiplier, 1 / multiplier); 1421 this.setWidth(origWidth).setHeight(origHeight); 1422 1423 if (activeObject) { 1424 this.setActiveObject(activeObject); 1425 } 1426 this.renderAll(); 1427 1428 return dataURL; 1429 }, 1430 1431 /** 1432 * Returns pointer coordinates relative to canvas. 1433 * @method getPointer 1434 * @return {Object} object with "x" and "y" number values 1435 */ 1436 getPointer: function (e) { 1437 var pointer = getPointer(e); 1438 return { 1439 x: pointer.x - this._offset.left, 1440 y: pointer.y - this._offset.top 1441 }; 1442 }, 1443 1444 /** 1445 * Returns coordinates of a center of canvas. 1446 * Returned value is an object with top and left properties 1447 * @method getCenter 1448 * @return {Object} object with "top" and "left" number values 1449 */ 1450 getCenter: function () { 1451 return { 1452 top: this.getHeight() / 2, 1453 left: this.getWidth() / 2 1454 }; 1455 }, 1456 1457 /** 1458 * Centers object horizontally. 1459 * @method centerObjectH 1460 * @param {fabric.Object} object Object to center 1461 * @return {fabric.Canvas} thisArg 1462 */ 1463 centerObjectH: function (object) { 1464 object.set('left', this.getCenter().left); 1465 this.renderAll(); 1466 return this; 1467 }, 1468 1469 /** 1470 * Centers object horizontally with animation. 1471 * @method fxCenterObjectH 1472 * @param {fabric.Object} object Object to center 1473 * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties 1474 * @return {fabric.Canvas} thisArg 1475 * @chainable 1476 */ 1477 fxCenterObjectH: function (object, callbacks) { 1478 callbacks = callbacks || { }; 1479 1480 var empty = function() { }, 1481 onComplete = callbacks.onComplete || empty, 1482 onChange = callbacks.onChange || empty, 1483 _this = this; 1484 1485 fabric.util.animate({ 1486 startValue: object.get('left'), 1487 endValue: this.getCenter().left, 1488 duration: this.FX_DURATION, 1489 onChange: function(value) { 1490 object.set('left', value); 1491 _this.renderAll(); 1492 onChange(); 1493 }, 1494 onComplete: function() { 1495 object.setCoords(); 1496 onComplete(); 1497 } 1498 }); 1499 1500 return this; 1501 }, 1502 1503 /** 1504 * Centers object vertically. 1505 * @method centerObjectH 1506 * @param {fabric.Object} object Object to center 1507 * @return {fabric.Canvas} thisArg 1508 * @chainable 1509 */ 1510 centerObjectV: function (object) { 1511 object.set('top', this.getCenter().top); 1512 this.renderAll(); 1513 return this; 1514 }, 1515 1516 /** 1517 * Centers object vertically with animation. 1518 * @method fxCenterObjectV 1519 * @param {fabric.Object} object Object to center 1520 * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties 1521 * @return {fabric.Canvas} thisArg 1522 * @chainable 1523 */ 1524 fxCenterObjectV: function (object, callbacks) { 1525 callbacks = callbacks || { }; 1526 1527 var empty = function() { }, 1528 onComplete = callbacks.onComplete || empty, 1529 onChange = callbacks.onChange || empty, 1530 _this = this; 1531 1532 fabric.util.animate({ 1533 startValue: object.get('top'), 1534 endValue: this.getCenter().top, 1535 duration: this.FX_DURATION, 1536 onChange: function(value) { 1537 object.set('top', value); 1538 _this.renderAll(); 1539 onChange(); 1540 }, 1541 onComplete: function() { 1542 object.setCoords(); 1543 onComplete(); 1544 } 1545 }); 1546 1547 return this; 1548 }, 1549 1550 /** 1551 * Straightens object, then rerenders canvas 1552 * @method straightenObject 1553 * @param {fabric.Object} object Object to straighten 1554 * @return {fabric.Canvas} thisArg 1555 * @chainable 1556 */ 1557 straightenObject: function (object) { 1558 object.straighten(); 1559 this.renderAll(); 1560 return this; 1561 }, 1562 1563 /** 1564 * Same as `fabric.Canvas#straightenObject`, but animated 1565 * @method fxStraightenObject 1566 * @param {fabric.Object} object Object to straighten 1567 * @return {fabric.Canvas} thisArg 1568 * @chainable 1569 */ 1570 fxStraightenObject: function (object) { 1571 object.fxStraighten({ 1572 onChange: this.renderAll.bind(this) 1573 }); 1574 return this; 1575 }, 1576 1577 /** 1578 * Returs dataless JSON representation of canvas 1579 * @method toDatalessJSON 1580 * @return {String} json string 1581 */ 1582 toDatalessJSON: function () { 1583 return this.toDatalessObject(); 1584 }, 1585 1586 /** 1587 * Returns object representation of canvas 1588 * @method toObject 1589 * @return {Object} 1590 */ 1591 toObject: function () { 1592 return this._toObjectMethod('toObject'); 1593 }, 1594 1595 /** 1596 * Returns dataless object representation of canvas 1597 * @method toDatalessObject 1598 * @return {Object} 1599 */ 1600 toDatalessObject: function () { 1601 return this._toObjectMethod('toDatalessObject'); 1602 }, 1603 1604 /** 1605 * @private 1606 * @method _toObjectMethod 1607 */ 1608 _toObjectMethod: function (methodName) { 1609 return { 1610 objects: this._objects.map(function (instance){ 1611 // TODO (kangax): figure out how to clean this up 1612 if (!this.includeDefaultValues) { 1613 var originalValue = instance.includeDefaultValues; 1614 instance.includeDefaultValues = false; 1615 } 1616 var object = instance[methodName](); 1617 if (!this.includeDefaultValues) { 1618 instance.includeDefaultValues = originalValue; 1619 } 1620 return object; 1621 }, this), 1622 background: this.backgroundColor 1623 } 1624 }, 1625 1626 /** 1627 * Returns true if canvas contains no objects 1628 * @method isEmpty 1629 * @return {Boolean} true if canvas is empty 1630 */ 1631 isEmpty: function () { 1632 return this._objects.length === 0; 1633 }, 1634 1635 /** 1636 * Populates canvas with data from the specified JSON 1637 * JSON format must conform to the one of `fabric.Canvas#toJSON` 1638 * @method loadFromJSON 1639 * @param {String} json JSON string 1640 * @param {Function} callback Callback, invoked when json is parsed 1641 * and corresponding objects (e.g: fabric.Image) 1642 * are initialized 1643 * @return {fabric.Canvas} instance 1644 * @chainable 1645 */ 1646 loadFromJSON: function (json, callback) { 1647 if (!json) return; 1648 1649 var serialized = JSON.parse(json); 1650 if (!serialized || (serialized && !serialized.objects)) return; 1651 1652 this.clear(); 1653 var _this = this; 1654 this._enlivenObjects(serialized.objects, function () { 1655 _this.backgroundColor = serialized.background; 1656 if (callback) { 1657 callback(); 1658 } 1659 }); 1660 1661 return this; 1662 }, 1663 1664 /** 1665 * @method _enlivenObjects 1666 * @param {Array} objects 1667 * @param {Function} callback 1668 */ 1669 _enlivenObjects: function (objects, callback) { 1670 var numLoadedImages = 0, 1671 // get length of all images 1672 numTotalImages = objects.filter(function (o) { 1673 return o.type === 'image'; 1674 }).length; 1675 1676 var _this = this; 1677 1678 objects.forEach(function (o, index) { 1679 if (!o.type) { 1680 return; 1681 } 1682 switch (o.type) { 1683 case 'image': 1684 case 'font': 1685 fabric[capitalize(o.type)].fromObject(o, function (o) { 1686 _this.insertAt(o, index); 1687 if (++numLoadedImages === numTotalImages) { 1688 if (callback) { 1689 callback(); 1690 } 1691 } 1692 }); 1693 break; 1694 default: 1695 var klass = fabric[camelize(capitalize(o.type))]; 1696 if (klass && klass.fromObject) { 1697 _this.insertAt(klass.fromObject(o), index); 1698 } 1699 break; 1700 } 1701 }); 1702 1703 if (numTotalImages === 0 && callback) { 1704 callback(); 1705 } 1706 }, 1707 1708 /** 1709 * Populates canvas with data from the specified dataless JSON 1710 * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` 1711 * @method loadFromDatalessJSON 1712 * @param {String} json JSON string 1713 * @param {Function} callback Callback, invoked when json is parsed 1714 * and corresponding objects (e.g: fabric.Image) 1715 * are initialized 1716 * @return {fabric.Canvas} instance 1717 * @chainable 1718 */ 1719 loadFromDatalessJSON: function (json, callback) { 1720 1721 if (!json) { 1722 return; 1723 } 1724 1725 // serialize if it wasn't already 1726 var serialized = (typeof json === 'string') 1727 ? JSON.parse(json) 1728 : json; 1729 1730 if (!serialized || (serialized && !serialized.objects)) return; 1731 1732 this.clear(); 1733 1734 // TODO: test this 1735 this.backgroundColor = serialized.background; 1736 this._enlivenDatalessObjects(serialized.objects, callback); 1737 }, 1738 1739 /** 1740 * @method _enlivenDatalessObjects 1741 * @param {Array} objects 1742 * @param {Function} callback 1743 */ 1744 _enlivenDatalessObjects: function (objects, callback) { 1745 1746 /** @ignore */ 1747 function onObjectLoaded(object, index) { 1748 _this.insertAt(object, index); 1749 object.setCoords(); 1750 if (++numLoadedObjects === numTotalObjects) { 1751 callback && callback(); 1752 } 1753 } 1754 1755 var _this = this, 1756 numLoadedObjects = 0, 1757 numTotalObjects = objects.length; 1758 1759 if (numTotalObjects === 0 && callback) { 1760 callback(); 1761 } 1762 1763 try { 1764 objects.forEach(function (obj, index) { 1765 1766 var pathProp = obj.paths ? 'paths' : 'path'; 1767 var path = obj[pathProp]; 1768 1769 delete obj[pathProp]; 1770 1771 if (typeof path !== 'string') { 1772 switch (obj.type) { 1773 case 'image': 1774 case 'text': 1775 fabric[capitalize(obj.type)].fromObject(obj, function (o) { 1776 onObjectLoaded(o, index); 1777 }); 1778 break; 1779 default: 1780 var klass = fabric[camelize(capitalize(obj.type))]; 1781 if (klass && klass.fromObject) { 1782 // restore path 1783 if (path) { 1784 obj[pathProp] = path; 1785 } 1786 onObjectLoaded(klass.fromObject(obj), index); 1787 } 1788 break; 1789 } 1790 } 1791 else { 1792 if (obj.type === 'image') { 1793 _this.loadImageFromURL(path, function (image) { 1794 image.setSourcePath(path); 1795 1796 extend(image, obj); 1797 image.setAngle(obj.angle); 1798 1799 onObjectLoaded(image, index); 1800 }); 1801 } 1802 else if (obj.type === 'text') { 1803 1804 obj.path = path; 1805 var object = fabric.Text.fromObject(obj); 1806 var onscriptload = function () { 1807 // TODO (kangax): find out why Opera refuses to work without this timeout 1808 if (Object.prototype.toString.call(window.opera) === '[object Opera]') { 1809 setTimeout(function () { 1810 onObjectLoaded(object, index); 1811 }, 500); 1812 } 1813 else { 1814 onObjectLoaded(object, index); 1815 } 1816 } 1817 1818 fabric.util.getScript(path, onscriptload); 1819 } 1820 else { 1821 _this.loadSVGFromURL(path, function (elements, options) { 1822 if (elements.length > 1) { 1823 var object = new fabric.PathGroup(elements, obj); 1824 } 1825 else { 1826 var object = elements[0]; 1827 } 1828 object.setSourcePath(path); 1829 1830 // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) 1831 // skip this step if an object is a PathGroup, since we already passed it options object before 1832 if (!(object instanceof fabric.PathGroup)) { 1833 extend(object, obj); 1834 if (typeof obj.angle !== 'undefined') { 1835 object.setAngle(obj.angle); 1836 } 1837 } 1838 1839 onObjectLoaded(object, index); 1840 }); 1841 } 1842 } 1843 }, this); 1844 } 1845 catch(e) { 1846 fabric.log(e.message); 1847 } 1848 }, 1849 1850 /** 1851 * Loads an image from URL, creates an instance of fabric.Image and passes it to a callback 1852 * @function 1853 * @method loadImageFromURL 1854 * @param url {String} url of image to load 1855 * @param callback {Function} calback, invoked when image is loaded 1856 */ 1857 loadImageFromURL: (function () { 1858 var imgCache = { }; 1859 1860 return function (url, callback) { 1861 // check cache first 1862 1863 var _this = this; 1864 1865 function checkIfLoaded() { 1866 var imgEl = document.getElementById(imgCache[url]); 1867 if (imgEl.width && imgEl.height) { 1868 callback(new fabric.Image(imgEl)); 1869 } 1870 else { 1871 setTimeout(checkIfLoaded, 50); 1872 } 1873 } 1874 1875 // get by id from cache 1876 if (imgCache[url]) { 1877 // id can be cached but image might still not be loaded, so we poll here 1878 checkIfLoaded(); 1879 } 1880 // else append a new image element 1881 else { 1882 var imgEl = new Image(); 1883 1884 /** @ignore */ 1885 imgEl.onload = function () { 1886 imgEl.onload = null; 1887 1888 if (imgEl.width && imgEl.height) { 1889 callback(new fabric.Image(imgEl)); 1890 } 1891 }; 1892 1893 imgEl.className = 'canvas-img-clone'; 1894 imgEl.style.cssText = 'position:absolute;left:-9999px;top:-9999px;'; 1895 imgEl.src = url; 1896 1897 if (this.shouldCacheImages) { 1898 // TODO (kangax): replace Element.identify w. fabric -based alternative 1899 imgCache[url] = Element.identify(imgEl); 1900 } 1901 document.body.appendChild(imgEl); 1902 } 1903 } 1904 })(), 1905 1906 /** 1907 * Takes url corresponding to an SVG document, and parses it to a set of objects 1908 * @method loadSVGFromURL 1909 * @param {String} url 1910 * @param {Function} callback 1911 */ 1912 loadSVGFromURL: function (url, callback) { 1913 1914 var _this = this; 1915 1916 url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); 1917 1918 this.cache.has(url, function (hasUrl) { 1919 if (hasUrl) { 1920 _this.cache.get(url, function (value) { 1921 var enlivedRecord = _this._enlivenCachedObject(value); 1922 callback(enlivedRecord.objects, enlivedRecord.options); 1923 }); 1924 } 1925 else { 1926 new fabric.util.request(url, { 1927 method: 'get', 1928 onComplete: onComplete 1929 }); 1930 } 1931 }); 1932 1933 function onComplete(r) { 1934 1935 var xml = r.responseXML; 1936 if (!xml) return; 1937 1938 var doc = xml.documentElement; 1939 if (!doc) return; 1940 1941 fabric.parseSVGDocument(doc, function (results, options) { 1942 _this.cache.set(url, { 1943 objects: fabric.util.array.invoke(results, 'toObject'), 1944 options: options 1945 }); 1946 callback(results, options); 1947 }); 1948 } 1949 }, 1950 1951 /** 1952 * @method _enlivenCachedObject 1953 */ 1954 _enlivenCachedObject: function (cachedObject) { 1955 1956 var objects = cachedObject.objects, 1957 options = cachedObject.options; 1958 1959 objects = objects.map(function (o) { 1960 return fabric[capitalize(o.type)].fromObject(o); 1961 }); 1962 1963 return ({ objects: objects, options: options }); 1964 }, 1965 1966 /** 1967 * Removes an object from canvas and returns it 1968 * @method remove 1969 * @param object {Object} Object to remove 1970 * @return {Object} removed object 1971 */ 1972 remove: function (object) { 1973 removeFromArray(this._objects, object); 1974 if (this.getActiveObject() === object) { 1975 this.removeActiveObject(); 1976 } 1977 this.renderAll(); 1978 return object; 1979 }, 1980 1981 /** 1982 * Same as `fabric.Canvas#remove` but animated 1983 * @method fxRemove 1984 * @param {fabric.Object} object Object to remove 1985 * @param {Function} callback Callback, invoked on effect completion 1986 * @return {fabric.Canvas} thisArg 1987 * @chainable 1988 */ 1989 fxRemove: function (object, callback) { 1990 var _this = this; 1991 object.fxRemove({ 1992 onChange: this.renderAll.bind(this), 1993 onComplete: function () { 1994 _this.remove(object); 1995 if (typeof callback === 'function') { 1996 callback(); 1997 } 1998 } 1999 }); 2000 return this; 2001 }, 2002 2003 /** 2004 * Moves an object to the bottom of the stack of drawn objects 2005 * @method sendToBack 2006 * @param object {fabric.Object} Object to send to back 2007 * @return {fabric.Canvas} thisArg 2008 * @chainable 2009 */ 2010 sendToBack: function (object) { 2011 removeFromArray(this._objects, object); 2012 this._objects.unshift(object); 2013 return this.renderAll(); 2014 }, 2015 2016 /** 2017 * Moves an object to the top of the stack of drawn objects 2018 * @method bringToFront 2019 * @param object {fabric.Object} Object to send 2020 * @return {fabric.Canvas} thisArg 2021 * @chainable 2022 */ 2023 bringToFront: function (object) { 2024 removeFromArray(this._objects, object); 2025 this._objects.push(object); 2026 return this.renderAll(); 2027 }, 2028 2029 /** 2030 * Moves an object one level down in stack of drawn objects 2031 * @method sendBackwards 2032 * @param object {fabric.Object} Object to send 2033 * @return {fabric.Canvas} thisArg 2034 * @chainable 2035 */ 2036 sendBackwards: function (object) { 2037 var idx = this._objects.indexOf(object), 2038 nextIntersectingIdx = idx; 2039 2040 // if object is not on the bottom of stack 2041 if (idx !== 0) { 2042 2043 // traverse down the stack looking for the nearest intersecting object 2044 for (var i=idx-1; i>=0; --i) { 2045 if (object.intersectsWithObject(this._objects[i])) { 2046 nextIntersectingIdx = i; 2047 break; 2048 } 2049 } 2050 removeFromArray(this._objects, object); 2051 this._objects.splice(nextIntersectingIdx, 0, object); 2052 } 2053 return this.renderAll(); 2054 }, 2055 2056 /** 2057 * Moves an object one level up in stack of drawn objects 2058 * @method sendForward 2059 * @param object {fabric.Object} Object to send 2060 * @return {fabric.Canvas} thisArg 2061 * @chainable 2062 */ 2063 bringForward: function (object) { 2064 var objects = this.getObjects(), 2065 idx = objects.indexOf(object), 2066 nextIntersectingIdx = idx; 2067 2068 2069 // if object is not on top of stack (last item in an array) 2070 if (idx !== objects.length-1) { 2071 2072 // traverse up the stack looking for the nearest intersecting object 2073 for (var i = idx + 1, l = this._objects.length; i < l; ++i) { 2074 if (object.intersectsWithObject(objects[i])) { 2075 nextIntersectingIdx = i; 2076 break; 2077 } 2078 } 2079 removeFromArray(objects, object); 2080 objects.splice(nextIntersectingIdx, 0, object); 2081 } 2082 this.renderAll(); 2083 }, 2084 2085 /** 2086 * Sets given object as active 2087 * @method setActiveObject 2088 * @param object {fabric.Object} Object to set as an active one 2089 * @return {fabric.Canvas} thisArg 2090 * @chainable 2091 */ 2092 setActiveObject: function (object) { 2093 if (this._activeObject) { 2094 this._activeObject.setActive(false); 2095 } 2096 this._activeObject = object; 2097 object.setActive(true); 2098 2099 this.renderAll(); 2100 2101 this.fire('object:selected', { target: object }); 2102 return this; 2103 }, 2104 2105 /** 2106 * Returns currently active object 2107 * @method getActiveObject 2108 * @return {fabric.Object} active object 2109 */ 2110 getActiveObject: function () { 2111 return this._activeObject; 2112 }, 2113 2114 /** 2115 * Removes currently active object 2116 * @method removeActiveObject 2117 * @return {fabric.Canvas} thisArg 2118 * @chainable 2119 */ 2120 removeActiveObject: function () { 2121 if (this._activeObject) { 2122 this._activeObject.setActive(false); 2123 } 2124 this._activeObject = null; 2125 return this; 2126 }, 2127 2128 /** 2129 * Sets active group to a speicified one 2130 * @method setActiveGroup 2131 * @param {fabric.Group} group Group to set as a current one 2132 * @return {fabric.Canvas} thisArg 2133 * @chainable 2134 */ 2135 setActiveGroup: function (group) { 2136 this._activeGroup = group; 2137 return this; 2138 }, 2139 2140 /** 2141 * Returns currently active group 2142 * @method getActiveGroup 2143 * @return {fabric.Group} Current group 2144 */ 2145 getActiveGroup: function () { 2146 return this._activeGroup; 2147 }, 2148 2149 /** 2150 * Removes currently active group 2151 * @method removeActiveGroup 2152 * @return {fabric.Canvas} thisArg 2153 */ 2154 removeActiveGroup: function () { 2155 var g = this.getActiveGroup(); 2156 if (g) { 2157 g.destroy(); 2158 } 2159 return this.setActiveGroup(null); 2160 }, 2161 2162 /** 2163 * Returns object at specified index 2164 * @method item 2165 * @param {Number} index 2166 * @return {fabric.Object} 2167 */ 2168 item: function (index) { 2169 return this.getObjects()[index]; 2170 }, 2171 2172 /** 2173 * Deactivates all objects by calling their setActive(false) 2174 * @method deactivateAll 2175 * @return {fabric.Canvas} thisArg 2176 */ 2177 deactivateAll: function () { 2178 var allObjects = this.getObjects(), 2179 i = 0, 2180 len = allObjects.length; 2181 for ( ; i < len; i++) { 2182 allObjects[i].setActive(false); 2183 } 2184 this.removeActiveGroup(); 2185 this.removeActiveObject(); 2186 return this; 2187 }, 2188 2189 /** 2190 * Returns number representation of an instance complexity 2191 * @method complexity 2192 * @return {Number} complexity 2193 */ 2194 complexity: function () { 2195 return this.getObjects().reduce(function (memo, current) { 2196 memo += current.complexity ? current.complexity() : 0; 2197 return memo; 2198 }, 0); 2199 }, 2200 2201 /** 2202 * Iterates over all objects, invoking callback for each one of them 2203 * @method forEachObject 2204 * @return {fabric.Canvas} thisArg 2205 */ 2206 forEachObject: function(callback, context) { 2207 var objects = this.getObjects(), 2208 i = objects.length; 2209 while (i--) { 2210 callback.call(context, objects[i], i, objects); 2211 } 2212 return this; 2213 }, 2214 2215 /** 2216 * Clears a canvas element and removes all event handlers. 2217 * @method dispose 2218 * @return {fabric.Canvas} thisArg 2219 * @chainable 2220 */ 2221 dispose: function () { 2222 this.clear(); 2223 removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); 2224 removeListener(document, 'mousemove', this._onMouseMove); 2225 removeListener(window, 'resize', this._onResize); 2226 return this; 2227 }, 2228 2229 /** 2230 * Clones canvas instance 2231 * @method clone 2232 * @param {Object} [callback] Expects `onBeforeClone` and `onAfterClone` functions 2233 * @return {fabric.Canvas} Clone of this instance 2234 */ 2235 clone: function (callback) { 2236 var el = document.createElement('canvas'); 2237 2238 el.width = this.getWidth(); 2239 el.height = this.getHeight(); 2240 2241 // cache 2242 var clone = this.__clone || (this.__clone = new fabric.Canvas(el)); 2243 clone.clipTo = this.clipTo; 2244 2245 return clone.loadFromJSON(JSON.stringify(this.toJSON()), function () { 2246 if (callback) { 2247 callback(clone); 2248 } 2249 }); 2250 }, 2251 2252 /** 2253 * @private 2254 * @method _toDataURL 2255 * @param {String} format 2256 * @param {Function} callback 2257 */ 2258 _toDataURL: function (format, callback) { 2259 this.clone(function (clone) { 2260 callback(clone.toDataURL(format)); 2261 }); 2262 }, 2263 2264 /** 2265 * @private 2266 * @method _toDataURLWithMultiplier 2267 * @param {String} format 2268 * @param {Number} multiplier 2269 * @param {Function} callback 2270 */ 2271 _toDataURLWithMultiplier: function (format, multiplier, callback) { 2272 this.clone(function (clone) { 2273 callback(clone.toDataURLWithMultiplier(format, multiplier)); 2274 }); 2275 }, 2276 2277 /** 2278 * @private 2279 * @method _resizeImageToFit 2280 * @param {HTMLImageElement} imgEl 2281 */ 2282 _resizeImageToFit: function (imgEl) { 2283 2284 var imageWidth = imgEl.width || imgEl.offsetWidth, 2285 widthScaleFactor = this.getWidth() / imageWidth; 2286 2287 // scale image down so that it has original dimensions when printed in large resolution 2288 if (imageWidth) { 2289 imgEl.width = imageWidth * widthScaleFactor; 2290 } 2291 }, 2292 2293 /** 2294 * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) 2295 * @property 2296 * @namespace 2297 */ 2298 cache: { 2299 2300 /** 2301 * @method has 2302 * @param {String} name 2303 * @param {Function} callback 2304 */ 2305 has: function (name, callback) { 2306 callback(false); 2307 }, 2308 2309 /** 2310 * @method get 2311 * @param {String} url 2312 * @param {Function} callback 2313 */ 2314 get: function (url, callback) { 2315 /* NOOP */ 2316 }, 2317 2318 /** 2319 * @method set 2320 * @param {String} url 2321 * @param {Object} object 2322 */ 2323 set: function (url, object) { 2324 /* NOOP */ 2325 } 2326 } 2327 }); 2328 2329 /** 2330 * Returns a string representation of an instance 2331 * @method toString 2332 * @return {String} string representation of an instance 2333 */ 2334 fabric.Canvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet 2335 return '#<fabric.Canvas (' + this.complexity() + '): '+ 2336 '{ objects: ' + this.getObjects().length + ' }>'; 2337 }; 2338 2339 extend(fabric.Canvas, /** @scope fabric.Canvas */ { 2340 2341 /** 2342 * @static 2343 * @property EMPTY_JSON 2344 * @type String 2345 */ 2346 EMPTY_JSON: '{"objects": [], "background": "white"}', 2347 2348 /** 2349 * Takes <canvas> element and transforms its data in such way that it becomes grayscale 2350 * @static 2351 * @method toGrayscale 2352 * @param {HTMLCanvasElement} canvasEl 2353 */ 2354 toGrayscale: function (canvasEl) { 2355 var context = canvasEl.getContext('2d'), 2356 imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), 2357 data = imageData.data, 2358 iLen = imageData.width, 2359 jLen = imageData.height, 2360 index, average, i, j; 2361 2362 for (i = 0; i < iLen; i++) { 2363 for (j = 0; j < jLen; j++) { 2364 2365 index = (i * 4) * jLen + (j * 4); 2366 average = (data[index] + data[index + 1] + data[index + 2]) / 3; 2367 2368 data[index] = average; 2369 data[index + 1] = average; 2370 data[index + 2] = average; 2371 } 2372 } 2373 2374 context.putImageData(imageData, 0, 0); 2375 }, 2376 2377 /** 2378 * Provides a way to check support of some of the canvas methods 2379 * (either those of HTMLCanvasElement itself, or rendering context) 2380 * 2381 * @method supports 2382 * @param methodName {String} Method to check support for; 2383 * Could be one of "getImageData" or "toDataURL" 2384 * @return {Boolean | null} `true` if method is supported (or at least exists), 2385 * `null` if canvas element or context can not be initialized 2386 */ 2387 supports: function (methodName) { 2388 var el = document.createElement('canvas'); 2389 2390 if (typeof G_vmlCanvasManager !== 'undefined') { 2391 G_vmlCanvasManager.initElement(el); 2392 } 2393 if (!el || !el.getContext) { 2394 return null; 2395 } 2396 2397 var ctx = el.getContext('2d'); 2398 if (!ctx) { 2399 return null; 2400 } 2401 2402 switch (methodName) { 2403 2404 case 'getImageData': 2405 return typeof ctx.getImageData !== 'undefined'; 2406 2407 case 'toDataURL': 2408 return typeof el.toDataURL !== 'undefined'; 2409 2410 default: 2411 return null; 2412 } 2413 } 2414 }); 2415 2416 /** 2417 * Returs JSON representation of canvas 2418 * @function 2419 * @method toJSON 2420 * @return {String} json string 2421 */ 2422 fabric.Canvas.prototype.toJSON = fabric.Canvas.prototype.toObject; 2423 2424 /** 2425 * @class fabric.Element 2426 * @alias fabric.Canvas 2427 * @deprecated 2428 * @constructor 2429 */ 2430 fabric.Element = fabric.Canvas; 2431 2432 })(this);