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