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