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