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