1 (function(global){ 2 3 "use strict"; 4 5 var fabric = global.fabric || (global.fabric = { }), 6 extend = fabric.util.object.extend, 7 clone = fabric.util.object.clone, 8 toFixed = fabric.util.toFixed, 9 capitalize = fabric.util.string.capitalize, 10 getPointer = fabric.util.getPointer, 11 slice = Array.prototype.slice 12 13 if (fabric.Object) { 14 return; 15 } 16 17 /** 18 * @class Object 19 * @memberOf fabric 20 */ 21 fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { 22 23 /** 24 * @property 25 * @type String 26 */ 27 type: 'object', 28 29 /** 30 * @property 31 * @type Boolean 32 */ 33 includeDefaultValues: true, 34 35 /** 36 * @constant 37 * @type Number 38 */ 39 NUM_FRACTION_DIGITS: 2, 40 41 /** 42 * @constant 43 * @type Number 44 */ 45 FX_DURATION: 500, 46 47 /** 48 * @constant 49 * @type String 50 */ 51 FX_TRANSITION: 'decel', 52 53 /** 54 * @constant 55 * @type Number 56 */ 57 MIN_SCALE_LIMIT: 0.1, 58 59 /** 60 * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); 61 * as well as for history (undo/redo) purposes 62 * @property 63 * @type Array 64 */ 65 stateProperties: ('top left width height scaleX scaleY flipX flipY ' + 66 'theta angle opacity cornersize fill overlayFill stroke ' + 67 'strokeWidth fillRule borderScaleFactor transformMatrix').split(' '), 68 69 // TODO (kangax): rename to `defaultOptions` 70 71 /** 72 * @property 73 * @type Object 74 */ 75 options: { 76 top: 0, 77 left: 0, 78 width: 100, 79 height: 100, 80 scaleX: 1, 81 scaleY: 1, 82 flipX: false, 83 flipY: false, 84 theta: 0, 85 opacity: 1, 86 angle: 0, 87 cornersize: 10, 88 padding: 0, 89 borderColor: 'rgba(102,153,255,0.75)', 90 cornerColor: 'rgba(102,153,255,0.5)', 91 fill: 'rgb(0,0,0)', 92 overlayFill: null, 93 stroke: null, 94 strokeWidth: 1, 95 fillRule: 'source-over', 96 borderOpacityWhenMoving: 0.4, 97 borderScaleFactor: 1, 98 transformMatrix: null 99 }, 100 101 /** 102 * @method callSuper 103 * @param {String} methodName 104 */ 105 callSuper: function(methodName) { 106 var fn = this.constructor.superclass.prototype[methodName]; 107 return (arguments.length > 1) 108 ? fn.apply(this, slice.call(arguments, 1)) 109 : fn.call(this); 110 }, 111 112 /** 113 * Constructor 114 * @method initialize 115 * @param {Object} [options] Options object 116 */ 117 initialize: function(options) { 118 // overwrite default options with specified ones 119 this.setOptions(options); 120 // "import" state properties into an instance 121 this._importProperties(); 122 // create "local" members 123 this.originalState = { }; 124 // set initial coords 125 this.setCoords(); 126 // setup state properties 127 this.saveState(); 128 }, 129 130 /** 131 * @method setOptions 132 * @param {Object} [options] 133 */ 134 setOptions: function(options) { 135 // this.constructor.superclass.prototype.options -> this.options -> options 136 this.options = extend(this._getOptions(), options); 137 }, 138 139 /** 140 * @private 141 * @method _getOptions 142 */ 143 _getOptions: function() { 144 return extend(clone(this._getSuperOptions()), this.options); 145 }, 146 147 /** 148 * @private 149 * @method _getSuperOptions 150 */ 151 _getSuperOptions: function() { 152 var c = this.constructor; 153 if (c) { 154 var s = c.superclass; 155 if (s) { 156 var p = s.prototype; 157 if (p && typeof p._getOptions == 'function') { 158 return p._getOptions(); 159 } 160 } 161 } 162 return { }; 163 }, 164 165 /** 166 * @private 167 * @method _importProperties 168 */ 169 _importProperties: function() { 170 this.stateProperties.forEach(function(prop) { 171 (prop === 'angle') 172 ? this.setAngle(this.options[prop]) 173 : (this[prop] = this.options[prop]); 174 }, this); 175 }, 176 177 /** 178 * @method transform 179 * @param {CanvasRenderingContext2D} ctx Context 180 */ 181 transform: function(ctx) { 182 ctx.globalAlpha = this.opacity; 183 ctx.translate(this.left, this.top); 184 ctx.rotate(this.theta); 185 ctx.scale( 186 this.scaleX * (this.flipX ? -1 : 1), 187 this.scaleY * (this.flipY ? -1 : 1) 188 ); 189 }, 190 191 /** 192 * Returns an object representation of an instance 193 * @method toObject 194 * @return {Object} 195 */ 196 toObject: function() { 197 var object = { 198 type: this.type, 199 left: toFixed(this.left, this.NUM_FRACTION_DIGITS), 200 top: toFixed(this.top, this.NUM_FRACTION_DIGITS), 201 width: toFixed(this.width, this.NUM_FRACTION_DIGITS), 202 height: toFixed(this.height, this.NUM_FRACTION_DIGITS), 203 fill: this.fill, 204 overlayFill: this.overlayFill, 205 stroke: this.stroke, 206 strokeWidth: this.strokeWidth, 207 scaleX: toFixed(this.scaleX, this.NUM_FRACTION_DIGITS), 208 scaleY: toFixed(this.scaleY, this.NUM_FRACTION_DIGITS), 209 angle: toFixed(this.getAngle(), this.NUM_FRACTION_DIGITS), 210 flipX: this.flipX, 211 flipY: this.flipY, 212 opacity: toFixed(this.opacity, this.NUM_FRACTION_DIGITS) 213 }; 214 215 if (!this.includeDefaultValues) { 216 object = this._removeDefaultValues(object); 217 } 218 return object; 219 }, 220 221 /** 222 * Returns (dataless) object representation of an instance 223 * @method toDatalessObject 224 */ 225 toDatalessObject: function() { 226 // will be overwritten by subclasses 227 return this.toObject(); 228 }, 229 230 /** 231 * @private 232 * @method _removeDefaultValues 233 */ 234 _removeDefaultValues: function(object) { 235 var defaultOptions = fabric.Object.prototype.options; 236 this.stateProperties.forEach(function(prop) { 237 if (object[prop] === defaultOptions[prop]) { 238 delete object[prop]; 239 } 240 }); 241 return object; 242 }, 243 244 /** 245 * Returns true if an object is in its active state 246 * @return {Boolean} true if an object is in its active state 247 */ 248 isActive: function() { 249 return !!this.active; 250 }, 251 252 /** 253 * Sets state of an object - `true` makes it active, `false` - inactive 254 * @param {Boolean} active 255 * @return {fabric.Object} thisArg 256 * @chainable 257 */ 258 setActive: function(active) { 259 this.active = !!active; 260 return this; 261 }, 262 263 /** 264 * Returns a string representation of an instance 265 * @return {String} 266 */ 267 toString: function() { 268 return "#<fabric." + capitalize(this.type) + ">"; 269 }, 270 271 /** 272 * Basic setter 273 * @param {Any} property 274 * @param {Any} value 275 * @return {fabric.Object} thisArg 276 * @chainable 277 */ 278 set: function(property, value) { 279 var shouldConstrainValue = (property === 'scaleX' || property === 'scaleY') && value < this.MIN_SCALE_LIMIT; 280 if (shouldConstrainValue) { 281 value = this.MIN_SCALE_LIMIT; 282 } 283 if (property === 'angle') { 284 this.setAngle(value); 285 } 286 else { 287 this[property] = value; 288 } 289 return this; 290 }, 291 292 /** 293 * Toggles specified property from `true` to `false` or from `false` to `true` 294 * @method toggle 295 * @param {String} property property to toggle 296 * @return {fabric.Object} thisArg 297 * @chainable 298 */ 299 toggle: function(property) { 300 var value = this.get(property); 301 if (typeof value === 'boolean') { 302 this.set(property, !value); 303 } 304 return this; 305 }, 306 307 /** 308 * @method setSourcePath 309 * @param {String} value 310 * @return {fabric.Object} thisArg 311 * @chainable 312 */ 313 setSourcePath: function(value) { 314 this.sourcePath = value; 315 return this; 316 }, 317 318 /** 319 * Basic getter 320 * @method get 321 * @param {Any} property 322 * @return {Any} value of a property 323 */ 324 get: function(property) { 325 return (property === 'angle') 326 ? this.getAngle() 327 : this[property]; 328 }, 329 330 /** 331 * @method render 332 * @param {CanvasRenderingContext2D} ctx context to render on 333 * @param {Boolean} noTransform 334 */ 335 render: function(ctx, noTransform) { 336 337 // do not render if width or height are zeros 338 if (this.width === 0 || this.height === 0) return; 339 340 ctx.save(); 341 342 var m = this.transformMatrix; 343 if (m) { 344 ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); 345 } 346 347 if (!noTransform) { 348 this.transform(ctx); 349 } 350 351 if (this.stroke) { 352 ctx.lineWidth = this.strokeWidth; 353 ctx.strokeStyle = this.stroke; 354 } 355 356 if (this.overlayFill) { 357 ctx.fillStyle = this.overlayFill; 358 } 359 else if (this.fill) { 360 ctx.fillStyle = this.fill; 361 } 362 363 this._render(ctx, noTransform); 364 365 if (this.active && !noTransform) { 366 this.drawBorders(ctx); 367 this.hideCorners || this.drawCorners(ctx); 368 } 369 ctx.restore(); 370 }, 371 372 /** 373 * Returns width of an object 374 * @method getWidth 375 * @return {Number} width value 376 */ 377 getWidth: function() { 378 return this.width * this.scaleX; 379 }, 380 381 /** 382 * Returns height of an object 383 * @method getHeight 384 * @return {Number} height value 385 */ 386 getHeight: function() { 387 return this.height * this.scaleY; 388 }, 389 390 /** 391 * Scales an object (equally by x and y) 392 * @method scale 393 * @param value {Number} scale factor 394 * @return {fabric.Object} thisArg 395 * @chainable 396 */ 397 scale: function(value) { 398 this.scaleX = value; 399 this.scaleY = value; 400 return this; 401 }, 402 403 /** 404 * Scales an object to a given width (scaling by x/y equally) 405 * @method scaleToWidth 406 * @param value {Number} new width value 407 * @return {fabric.Object} thisArg 408 * @chainable 409 */ 410 scaleToWidth: function(value) { 411 return this.scale(value / this.width); 412 }, 413 414 /** 415 * Scales an object to a given height (scaling by x/y equally) 416 * @method scaleToHeight 417 * @param value {Number} new height value 418 * @return {fabric.Object} thisArg 419 * @chainable 420 */ 421 scaleToHeight: function(value) { 422 return this.scale(value / this.height); 423 }, 424 425 /** 426 * Sets object opacity 427 * @method setOpacity 428 * @param value {Number} value 0-1 429 * @return {fabric.Object} thisArg 430 * @chainable 431 */ 432 setOpacity: function(value) { 433 this.set('opacity', value); 434 return this; 435 }, 436 437 /** 438 * Returns object's angle value 439 * @method getAngle 440 * @return {Number} angle value 441 */ 442 getAngle: function() { 443 return this.theta * 180 / Math.PI; 444 }, 445 446 /** 447 * Sets object's angle 448 * @method setAngle 449 * @param value {Number} angle value 450 * @return {Object} thisArg 451 */ 452 setAngle: function(value) { 453 this.theta = value / 180 * Math.PI; 454 this.angle = value; 455 return this; 456 }, 457 458 /** 459 * Sets corner position coordinates based on current angle, width and height. 460 * @method setCoords 461 * return {fabric.Object} thisArg 462 * @chainable 463 */ 464 setCoords: function() { 465 466 this.currentWidth = this.width * this.scaleX; 467 this.currentHeight = this.height * this.scaleY; 468 469 this._hypotenuse = Math.sqrt( 470 Math.pow(this.currentWidth / 2, 2) + 471 Math.pow(this.currentHeight / 2, 2)); 472 473 this._angle = Math.atan(this.currentHeight / this.currentWidth); 474 475 // offset added for rotate and scale actions 476 var offsetX = Math.cos(this._angle + this.theta) * this._hypotenuse, 477 offsetY = Math.sin(this._angle + this.theta) * this._hypotenuse, 478 theta = this.theta, 479 sinTh = Math.sin(theta), 480 cosTh = Math.cos(theta); 481 482 var tl = { 483 x: this.left - offsetX, 484 y: this.top - offsetY 485 }; 486 var tr = { 487 x: tl.x + (this.currentWidth * cosTh), 488 y: tl.y + (this.currentWidth * sinTh) 489 }; 490 var br = { 491 x: tr.x - (this.currentHeight * sinTh), 492 y: tr.y + (this.currentHeight * cosTh) 493 }; 494 var bl = { 495 x: tl.x - (this.currentHeight * sinTh), 496 y: tl.y + (this.currentHeight * cosTh) 497 }; 498 var ml = { 499 x: tl.x - (this.currentHeight/2 * sinTh), 500 y: tl.y + (this.currentHeight/2 * cosTh) 501 }; 502 var mt = { 503 x: tl.x + (this.currentWidth/2 * cosTh), 504 y: tl.y + (this.currentWidth/2 * sinTh) 505 }; 506 var mr = { 507 x: tr.x - (this.currentHeight/2 * sinTh), 508 y: tr.y + (this.currentHeight/2 * cosTh) 509 } 510 var mb = { 511 x: bl.x + (this.currentWidth/2 * cosTh), 512 y: bl.y + (this.currentWidth/2 * sinTh) 513 } 514 515 // clockwise 516 this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb }; 517 518 // set coordinates of the draggable boxes in the corners used to scale/rotate the image 519 this._setCornerCoords(); 520 521 return this; 522 }, 523 524 /** 525 * Draws borders of an object's bounding box. 526 * Requires public properties: width, height 527 * Requires public options: padding, borderColor 528 * @method drawBorders 529 * @param {CanvasRenderingContext2D} ctx Context to draw on 530 * @return {fabric.Object} thisArg 531 * @chainable 532 */ 533 drawBorders: function(ctx) { 534 var o = this.options, 535 padding = o.padding, 536 padding2 = padding * 2; 537 538 ctx.save(); 539 540 ctx.globalAlpha = this.isMoving ? o.borderOpacityWhenMoving : 1; 541 ctx.strokeStyle = o.borderColor; 542 543 var scaleX = 1 / (this.scaleX < this.MIN_SCALE_LIMIT ? this.MIN_SCALE_LIMIT : this.scaleX), 544 scaleY = 1 / (this.scaleY < this.MIN_SCALE_LIMIT ? this.MIN_SCALE_LIMIT : this.scaleY); 545 546 // could be set by a group, that this object is contained within 547 ctx.lineWidth = 1 / this.borderScaleFactor; 548 549 ctx.scale(scaleX, scaleY); 550 551 var w = this.getWidth(), 552 h = this.getHeight(); 553 554 ctx.strokeRect( 555 ~~(-(w / 2) - padding) + 0.5, // offset needed to make lines look sharper 556 ~~(-(h / 2) - padding) + 0.5, 557 ~~(w + padding2), 558 ~~(h + padding2) 559 ); 560 561 ctx.restore(); 562 return this; 563 }, 564 565 /** 566 * Draws corners of an object's bounding box. 567 * Requires public properties: width, height, scaleX, scaleY 568 * Requires public options: cornersize, padding 569 * @method drawCorners 570 * @param {CanvasRenderingContext2D} ctx Context to draw on 571 * @return {fabric.Object} thisArg 572 * @chainable 573 */ 574 drawCorners: function(ctx) { 575 var size = this.options.cornersize, 576 size2 = size / 2, 577 padding = this.options.padding, 578 left = -(this.width / 2), 579 top = -(this.height / 2), 580 _left, 581 _top, 582 sizeX = size / this.scaleX, 583 sizeY = size / this.scaleY, 584 scaleOffsetY = (padding + size2) / this.scaleY, 585 scaleOffsetX = (padding + size2) / this.scaleX, 586 scaleOffsetSizeX = (padding + size2 - size) / this.scaleX, 587 scaleOffsetSizeY = (padding + size2 - size) / this.scaleY; 588 589 ctx.save(); 590 591 ctx.globalAlpha = this.isMoving ? this.options.borderOpacityWhenMoving : 1; 592 ctx.fillStyle = this.options.cornerColor; 593 594 // top-left 595 _left = left - scaleOffsetX; 596 _top = top - scaleOffsetY; 597 ctx.fillRect(_left, _top, sizeX, sizeY); 598 599 // top-right 600 _left = left + this.width - scaleOffsetX; 601 _top = top - scaleOffsetY; 602 ctx.fillRect(_left, _top, sizeX, sizeY); 603 604 // bottom-left 605 _left = left - scaleOffsetX; 606 _top = top + this.height + scaleOffsetSizeY; 607 ctx.fillRect(_left, _top, sizeX, sizeY); 608 609 // bottom-right 610 _left = left + this.width + scaleOffsetSizeX; 611 _top = top + this.height + scaleOffsetSizeY; 612 ctx.fillRect(_left, _top, sizeX, sizeY); 613 614 // middle-top 615 _left = left + this.width/2 - scaleOffsetX; 616 _top = top - scaleOffsetY; 617 ctx.fillRect(_left, _top, sizeX, sizeY); 618 619 // middle-bottom 620 _left = left + this.width/2 - scaleOffsetX; 621 _top = top + this.height + scaleOffsetSizeY; 622 ctx.fillRect(_left, _top, sizeX, sizeY); 623 624 // middle-right 625 _left = left + this.width + scaleOffsetSizeX; 626 _top = top + this.height/2 - scaleOffsetY; 627 ctx.fillRect(_left, _top, sizeX, sizeY); 628 629 // middle-left 630 _left = left - scaleOffsetX; 631 _top = top + this.height/2 - scaleOffsetY; 632 ctx.fillRect(_left, _top, sizeX, sizeY); 633 634 ctx.restore(); 635 636 return this; 637 }, 638 639 /** 640 * Clones an instance 641 * @method clone 642 * @param {Object} options object 643 * @return {fabric.Object} clone of an instance 644 */ 645 clone: function(options) { 646 if (this.constructor.fromObject) { 647 return this.constructor.fromObject(this.toObject(), options); 648 } 649 return new fabric.Object(this.toObject()); 650 }, 651 652 /** 653 * Creates an instance of fabric.Image out of an object 654 * @method cloneAsImage 655 * @param callback {Function} callback, invoked with an instance as a first argument 656 * @return {fabric.Object} thisArg 657 * @chainable 658 */ 659 cloneAsImage: function(callback) { 660 if (fabric.Image) { 661 var i = new Image(); 662 663 /** @ignore */ 664 i.onload = function() { 665 if (callback) { 666 callback(new fabric.Image(i), orig); 667 } 668 i = i.onload = null; 669 }; 670 671 var orig = { 672 angle: this.get('angle'), 673 flipX: this.get('flipX'), 674 flipY: this.get('flipY') 675 }; 676 677 // normalize angle 678 this.set('angle', 0).set('flipX', false).set('flipY', false); 679 i.src = this.toDataURL(); 680 } 681 return this; 682 }, 683 684 /** 685 * Converts an object into a data-url-like string 686 * @method toDataURL 687 * @return {String} string of data 688 */ 689 toDataURL: function() { 690 var el = document.createElement('canvas'); 691 692 el.width = this.getWidth(); 693 el.height = this.getHeight(); 694 695 fabric.util.wrapElement(el, 'div'); 696 697 var canvas = new fabric.Element(el); 698 canvas.backgroundColor = 'transparent'; 699 canvas.renderAll(); 700 701 var clone = this.clone(); 702 clone.left = el.width / 2; 703 clone.top = el.height / 2; 704 705 clone.setActive(false); 706 707 canvas.add(clone); 708 var data = canvas.toDataURL('png'); 709 710 canvas.dispose(); 711 canvas = clone = null; 712 return data; 713 }, 714 715 /** 716 * @method hasStateChanged 717 * @return {Boolean} true if instance' state has changed 718 */ 719 hasStateChanged: function() { 720 return this.stateProperties.some(function(prop) { 721 return this[prop] !== this.originalState[prop]; 722 }, this); 723 }, 724 725 /** 726 * @method saveState 727 * @return {fabric.Object} thisArg 728 * @chainable 729 */ 730 saveState: function() { 731 this.stateProperties.forEach(function(prop) { 732 this.originalState[prop] = this.get(prop); 733 }, this); 734 return this; 735 }, 736 737 /** 738 * Returns true if object intersects with an area formed by 2 points 739 * @method intersectsWithRect 740 * @param {Object} selectionTL 741 * @param {Object} selectionBR 742 * @return {Boolean} 743 */ 744 intersectsWithRect: function(selectionTL, selectionBR) { 745 var oCoords = this.oCoords, 746 tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y), 747 tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y), 748 bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y), 749 br = new fabric.Point(oCoords.br.x, oCoords.br.y); 750 751 var intersection = fabric.Intersection.intersectPolygonRectangle( 752 [tl, tr, br, bl], 753 selectionTL, 754 selectionBR 755 ); 756 return (intersection.status === 'Intersection'); 757 }, 758 759 /** 760 * Returns true if object intersects with another object 761 * @method intersectsWithObject 762 * @param {Object} other Object to test 763 * @return {Boolean} 764 */ 765 intersectsWithObject: function(other) { 766 // extracts coords 767 function getCoords(oCoords) { 768 return { 769 tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y), 770 tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y), 771 bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y), 772 br: new fabric.Point(oCoords.br.x, oCoords.br.y) 773 } 774 } 775 var thisCoords = getCoords(this.oCoords), 776 otherCoords = getCoords(other.oCoords); 777 var intersection = fabric.Intersection.intersectPolygonPolygon( 778 [thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl], 779 [otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl] 780 ); 781 782 return (intersection.status === 'Intersection'); 783 }, 784 785 /** 786 * Returns true if object is fully contained within area formed by 2 points 787 * @method isContainedWithinRect 788 * @param {Object} selectionTL 789 * @param {Object} selectionBR 790 * @return {Boolean} 791 */ 792 isContainedWithinRect: function(selectionTL, selectionBR) { 793 var oCoords = this.oCoords, 794 tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y), 795 tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y), 796 bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y), 797 br = new fabric.Point(oCoords.br.x, oCoords.br.y); 798 return tl.x > selectionTL.x 799 && tr.x < selectionBR.x 800 && tl.y > selectionTL.y 801 && bl.y < selectionBR.y; 802 }, 803 804 /** 805 * @method isType 806 * @param type {String} type to check against 807 * @return {Boolean} true if specified type is identical to the type of instance 808 */ 809 isType: function(type) { 810 return this.type === type; 811 }, 812 813 /** 814 * Determines which one of the four corners has been clicked 815 * @method _findTargetCorner 816 * @private 817 * @param e {Event} event object 818 * @param offset {Object} canvas offset 819 * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found 820 */ 821 _findTargetCorner: function(e, offset) { 822 var pointer = getPointer(e), 823 ex = pointer.x - offset.left, 824 ey = pointer.y - offset.top, 825 xpoints, 826 lines; 827 828 for (var i in this.oCoords) { 829 lines = this._getImageLines(this.oCoords[i].corner, i); 830 xpoints = this._findCrossPoints(ex, ey, lines); 831 if (xpoints % 2 == 1 && xpoints != 0) { 832 this.__corner = i; 833 return i; 834 } 835 } 836 return false; 837 }, 838 839 /** 840 * Helper method to determine how many cross points are between the 4 image edges 841 * and the horizontal line determined by the position of our mouse when clicked on canvas 842 * @method _findCrossPoints 843 * @private 844 * @param ex {Number} x coordinate of the mouse 845 * @param ey {Number} y coordinate of the mouse 846 * @param oCoords {Object} Coordinates of the image being evaluated 847 */ 848 _findCrossPoints: function(ex, ey, oCoords) { 849 var b1, b2, a1, a2, xi, yi, 850 xcount = 0, 851 iLine; 852 853 for (var lineKey in oCoords) { 854 iLine = oCoords[lineKey]; 855 // optimisation 1: line below dot. no cross 856 if ((iLine.o.y < ey) && (iLine.d.y < ey)) { 857 continue; 858 } 859 // optimisation 2: line above dot. no cross 860 if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { 861 continue; 862 } 863 // optimisation 3: vertical line case 864 if ((iLine.o.x == iLine.d.x) && (iLine.o.x >= ex)) { 865 xi = iLine.o.x; 866 yi = ey; 867 } 868 // calculate the intersection point 869 else { 870 b1 = 0; 871 b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x); 872 a1 = ey-b1*ex; 873 a2 = iLine.o.y-b2*iLine.o.x; 874 875 xi = - (a1-a2)/(b1-b2); 876 yi = a1+b1*xi; 877 } 878 // dont count xi < ex cases 879 if (xi >= ex) { 880 xcount += 1; 881 } 882 // optimisation 4: specific for square images 883 if (xcount == 2) { 884 break; 885 } 886 } 887 return xcount; 888 }, 889 890 /** 891 * Method that returns an object with the image lines in it given the coordinates of the corners 892 * @method _getImageLines 893 * @private 894 * @param oCoords {Object} coordinates of the image corners 895 */ 896 _getImageLines: function(oCoords, i) { 897 return { 898 topline: { 899 o: oCoords.tl, 900 d: oCoords.tr 901 }, 902 rightline: { 903 o: oCoords.tr, 904 d: oCoords.br 905 }, 906 bottomline: { 907 o: oCoords.br, 908 d: oCoords.bl 909 }, 910 leftline: { 911 o: oCoords.bl, 912 d: oCoords.tl 913 } 914 } 915 }, 916 917 /** 918 * Sets the coordinates of the draggable boxes in the corners of 919 * the image used to scale/rotate it. 920 * @method _setCornerCoords 921 * @private 922 */ 923 _setCornerCoords: function() { 924 var coords = this.oCoords, 925 theta = this.theta, 926 cosOffset = this.cornersize * /*this.scaleX * */ Math.cos(theta), 927 sinOffset = this.cornersize * /*this.scaleY * */ Math.sin(theta), 928 size2 = this.cornersize / 2, 929 size2x = size2 - sinOffset, 930 size2y = size2, 931 corner; 932 933 coords.tl.x -= size2x; 934 coords.tl.y -= size2y; 935 936 coords.tl.corner = { 937 tl: { 938 x: coords.tl.x, 939 y: coords.tl.y 940 }, 941 tr: { 942 x: coords.tl.x + cosOffset, 943 y: coords.tl.y + sinOffset 944 }, 945 bl: { 946 x: coords.tl.x - sinOffset, 947 y: coords.tl.y + cosOffset 948 } 949 }; 950 coords.tl.corner.br = { 951 x: coords.tl.corner.tr.x - sinOffset, 952 y: coords.tl.corner.tr.y + cosOffset 953 }; 954 955 coords.tl.x += size2x; 956 coords.tl.y += size2y; 957 958 coords.tr.x += size2; 959 coords.tr.y -= size2; 960 coords.tr.corner = { 961 tl: { 962 x: coords.tr.x - cosOffset, 963 y: coords.tr.y - sinOffset 964 }, 965 tr: { 966 x: coords.tr.x, 967 y: coords.tr.y 968 }, 969 br: { 970 x: coords.tr.x - sinOffset, 971 y: coords.tr.y + cosOffset 972 } 973 }; 974 coords.tr.corner.bl = { 975 x: coords.tr.corner.tl.x - sinOffset, 976 y: coords.tr.corner.tl.y + cosOffset 977 }; 978 coords.tr.x -= size2; 979 coords.tr.y += size2; 980 981 coords.bl.x -= size2; 982 coords.bl.y += size2; 983 coords.bl.corner = { 984 tl: { 985 x: coords.bl.x + sinOffset, 986 y: coords.bl.y - cosOffset 987 }, 988 bl: { 989 x: coords.bl.x, 990 y: coords.bl.y 991 }, 992 br: { 993 x: coords.bl.x + cosOffset, 994 y: coords.bl.y + sinOffset 995 } 996 }; 997 coords.bl.corner.tr = { 998 x: coords.bl.corner.br.x + sinOffset, 999 y: coords.bl.corner.br.y - cosOffset 1000 }; 1001 coords.bl.x += size2; 1002 coords.bl.y -= size2; 1003 1004 coords.br.x += size2; 1005 coords.br.y += size2; 1006 coords.br.corner = { 1007 tr: { 1008 x: coords.br.x + sinOffset, 1009 y: coords.br.y - cosOffset 1010 }, 1011 bl: { 1012 x: coords.br.x - cosOffset, 1013 y: coords.br.y - sinOffset 1014 }, 1015 br: { 1016 x: coords.br.x, 1017 y: coords.br.y 1018 } 1019 }; 1020 coords.br.corner.tl = { 1021 x: coords.br.corner.bl.x + sinOffset, 1022 y: coords.br.corner.bl.y - cosOffset 1023 }; 1024 coords.br.x -= size2; 1025 coords.br.y -= size2; 1026 1027 1028 coords.ml.x -= size2; 1029 coords.ml.y -= size2; 1030 coords.ml.corner = { 1031 tl: { 1032 x: coords.ml.x, 1033 y: coords.ml.y 1034 }, 1035 tr: { 1036 x: coords.ml.x + cosOffset, 1037 y: coords.ml.y + sinOffset 1038 }, 1039 bl: { 1040 x: coords.ml.x - sinOffset, 1041 y: coords.ml.y + cosOffset 1042 } 1043 }; 1044 coords.ml.corner.br = { 1045 x: coords.ml.corner.tr.x - sinOffset, 1046 y: coords.ml.corner.tr.y + cosOffset 1047 }; 1048 coords.ml.x += size2; 1049 coords.ml.y += size2; 1050 1051 coords.mt.x -= size2; 1052 coords.mt.y -= size2; 1053 coords.mt.corner = { 1054 tl: { 1055 x: coords.mt.x, 1056 y: coords.mt.y 1057 }, 1058 tr: { 1059 x: coords.mt.x + cosOffset, 1060 y: coords.mt.y + sinOffset 1061 }, 1062 bl: { 1063 x: coords.mt.x - sinOffset, 1064 y: coords.mt.y + cosOffset 1065 } 1066 }; 1067 coords.mt.corner.br = { 1068 x: coords.mt.corner.tr.x - sinOffset, 1069 y: coords.mt.corner.tr.y + cosOffset 1070 }; 1071 coords.mt.x += size2; 1072 coords.mt.y += size2; 1073 1074 coords.mr.x -= size2; 1075 coords.mr.y -= size2; 1076 coords.mr.corner = { 1077 tl: { 1078 x: coords.mr.x, 1079 y: coords.mr.y 1080 }, 1081 tr: { 1082 x: coords.mr.x + cosOffset, 1083 y: coords.mr.y + sinOffset 1084 }, 1085 bl: { 1086 x: coords.mr.x - sinOffset, 1087 y: coords.mr.y + cosOffset 1088 } 1089 }; 1090 coords.mr.corner.br = { 1091 x: coords.mr.corner.tr.x - sinOffset, 1092 y: coords.mr.corner.tr.y + cosOffset 1093 }; 1094 coords.mr.x += size2; 1095 coords.mr.y += size2; 1096 1097 coords.mb.x -= size2; 1098 coords.mb.y -= size2; 1099 coords.mb.corner = { 1100 tl: { 1101 x: coords.mb.x, 1102 y: coords.mb.y 1103 }, 1104 tr: { 1105 x: coords.mb.x + cosOffset, 1106 y: coords.mb.y + sinOffset 1107 }, 1108 bl: { 1109 x: coords.mb.x - sinOffset, 1110 y: coords.mb.y + cosOffset 1111 } 1112 }; 1113 coords.mb.corner.br = { 1114 x: coords.mb.corner.tr.x - sinOffset, 1115 y: coords.mb.corner.tr.y + cosOffset 1116 }; 1117 1118 coords.mb.x += size2; 1119 coords.mb.y += size2; 1120 1121 corner = coords.mb.corner; 1122 1123 corner.tl.x -= size2; 1124 corner.tl.y -= size2; 1125 corner.tr.x -= size2; 1126 corner.tr.y -= size2; 1127 corner.br.x -= size2; 1128 corner.br.y -= size2; 1129 corner.bl.x -= size2; 1130 corner.bl.y -= size2; 1131 }, 1132 1133 /** 1134 * Makes object's color grayscale 1135 * @method toGrayscale 1136 * @return {fabric.Object} thisArg 1137 */ 1138 toGrayscale: function() { 1139 var fillValue = this.get('fill'); 1140 if (fillValue) { 1141 this.set('overlayFill', new fabric.Color(fillValue).toGrayscale().toRgb()); 1142 } 1143 return this; 1144 }, 1145 1146 /** 1147 * @method complexity 1148 * @return {Number} 1149 */ 1150 complexity: function() { 1151 return 0; 1152 }, 1153 1154 /** 1155 * @method getCenter 1156 * @return {Object} object with `x`, `y` properties corresponding to path center coordinates 1157 */ 1158 getCenter: function() { 1159 return { 1160 x: this.get('left') + this.width / 2, 1161 y: this.get('top') + this.height / 2 1162 }; 1163 }, 1164 1165 /** 1166 * @method straighten 1167 * @return {fabric.Object} thisArg 1168 * @chainable 1169 */ 1170 straighten: function() { 1171 var angle = this._getAngleValueForStraighten(); 1172 this.setAngle(angle); 1173 return this; 1174 }, 1175 1176 /** 1177 * @method fxStraighten 1178 * @param {Object} callbacks 1179 * - onComplete: invoked on completion 1180 * - onChange: invoked on every step of animation 1181 * 1182 * @return {fabric.Object} thisArg 1183 * @chainable 1184 */ 1185 fxStraighten: function(callbacks) { 1186 callbacks = callbacks || { }; 1187 1188 var empty = function() { }, 1189 onComplete = callbacks.onComplete || empty, 1190 onChange = callbacks.onChange || empty, 1191 _this = this; 1192 1193 fabric.util.animate({ 1194 startValue: this.get('angle'), 1195 endValue: this._getAngleValueForStraighten(), 1196 duration: this.FX_DURATION, 1197 onChange: function(value) { 1198 _this.setAngle(value); 1199 onChange(); 1200 }, 1201 onComplete: function() { 1202 _this.setCoords(); 1203 onComplete(); 1204 }, 1205 onStart: function() { 1206 _this.setActive(false); 1207 } 1208 }); 1209 1210 return this; 1211 }, 1212 1213 /** 1214 * @method fxRemove 1215 * @param {Object} callbacks 1216 * @return {fabric.Object} thisArg 1217 * @chainable 1218 */ 1219 fxRemove: function(callbacks) { 1220 callbacks || (callbacks = { }); 1221 1222 var empty = function() { }, 1223 onComplete = callbacks.onComplete || empty, 1224 onChange = callbacks.onChange || empty, 1225 _this = this; 1226 1227 fabric.util.animate({ 1228 startValue: this.get('opacity'), 1229 endValue: 0, 1230 duration: this.FX_DURATION, 1231 onChange: function(value) { 1232 _this.set('opacity', value); 1233 onChange(); 1234 }, 1235 onComplete: onComplete, 1236 onStart: function() { 1237 _this.setActive(false); 1238 } 1239 }); 1240 1241 return this; 1242 }, 1243 1244 /** 1245 * @method _getAngleValueForStraighten 1246 * @return {Number} angle value 1247 * @private 1248 */ 1249 _getAngleValueForStraighten: function() { 1250 var angle = this.get('angle'); 1251 1252 // TODO (kangax): can this be simplified? 1253 1254 if (angle > -225 && angle <= -135) { return -180; } 1255 else if (angle > -135 && angle <= -45) { return -90; } 1256 else if (angle > -45 && angle <= 45) { return 0; } 1257 else if (angle > 45 && angle <= 135) { return 90; } 1258 else if (angle > 135 && angle <= 225 ) { return 180; } 1259 else if (angle > 225 && angle <= 315) { return 270; } 1260 else if (angle > 315) { return 360; } 1261 1262 return 0; 1263 }, 1264 1265 /** 1266 * Returns a JSON representation of an instance 1267 * @method toJSON 1268 * @return {String} json 1269 */ 1270 toJSON: function() { 1271 // delegate, not alias 1272 return this.toObject(); 1273 } 1274 }); 1275 1276 /** 1277 * @alias rotate -> setAngle 1278 */ 1279 fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; 1280 })(this);