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