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