(function(){ var getPointer = fabric.util.getPointer, degreesToRadians = fabric.util.degreesToRadians; fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ { /** * Determines which one of the four corners has been clicked * @method _findTargetCorner * @private * @param e {Event} event object * @param offset {Object} canvas offset * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(e, offset) { if (!this.hasControls || !this.active) return false; var pointer = getPointer(e, this.canvas.upperCanvasEl), ex = pointer.x - offset.left, ey = pointer.y - offset.top, xpoints, lines; for (var i in this.oCoords) { if (i === 'mtr' && !this.hasRotatingPoint) { continue; } if (this.get('lockUniScaling') && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { continue; } lines = this._getImageLines(this.oCoords[i].corner, i); // debugging // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); xpoints = this._findCrossPoints(ex, ey, lines); if (xpoints % 2 === 1 && xpoints !== 0) { this.__corner = i; return i; } } return false; }, /** * Helper method to determine how many cross points are between the 4 image edges * and the horizontal line determined by the position of our mouse when clicked on canvas * @method _findCrossPoints * @private * @param ex {Number} x coordinate of the mouse * @param ey {Number} y coordinate of the mouse * @param oCoords {Object} Coordinates of the image being evaluated */ _findCrossPoints: function(ex, ey, oCoords) { var b1, b2, a1, a2, xi, yi, xcount = 0, iLine; for (var lineKey in oCoords) { iLine = oCoords[lineKey]; // optimisation 1: line below dot. no cross if ((iLine.o.y < ey) && (iLine.d.y < ey)) { continue; } // optimisation 2: line above dot. no cross if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { continue; } // optimisation 3: vertical line case if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { xi = iLine.o.x; yi = ey; } // calculate the intersection point else { b1 = 0; b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x); a1 = ey-b1*ex; a2 = iLine.o.y-b2*iLine.o.x; xi = - (a1-a2)/(b1-b2); yi = a1+b1*xi; } // dont count xi < ex cases if (xi >= ex) { xcount += 1; } // optimisation 4: specific for square images if (xcount === 2) { break; } } return xcount; }, /** * Method that returns an object with the image lines in it given the coordinates of the corners * @method _getImageLines * @private * @param oCoords {Object} coordinates of the image corners */ _getImageLines: function(oCoords) { return { topline: { o: oCoords.tl, d: oCoords.tr }, rightline: { o: oCoords.tr, d: oCoords.br }, bottomline: { o: oCoords.br, d: oCoords.bl }, leftline: { o: oCoords.bl, d: oCoords.tl } }; }, /** * Sets the coordinates of the draggable boxes in the corners of * the image used to scale/rotate it. * @method _setCornerCoords * @private */ _setCornerCoords: function() { var coords = this.oCoords, theta = degreesToRadians(this.angle), newTheta = degreesToRadians(45 - this.angle), cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, cosHalfOffset = cornerHypotenuse * Math.cos(newTheta), sinHalfOffset = cornerHypotenuse * Math.sin(newTheta), sinTh = Math.sin(theta), cosTh = Math.cos(theta); coords.tl.corner = { tl: { x: coords.tl.x - sinHalfOffset, y: coords.tl.y - cosHalfOffset }, tr: { x: coords.tl.x + cosHalfOffset, y: coords.tl.y - sinHalfOffset }, bl: { x: coords.tl.x - cosHalfOffset, y: coords.tl.y + sinHalfOffset }, br: { x: coords.tl.x + sinHalfOffset, y: coords.tl.y + cosHalfOffset } }; coords.tr.corner = { tl: { x: coords.tr.x - sinHalfOffset, y: coords.tr.y - cosHalfOffset }, tr: { x: coords.tr.x + cosHalfOffset, y: coords.tr.y - sinHalfOffset }, br: { x: coords.tr.x + sinHalfOffset, y: coords.tr.y + cosHalfOffset }, bl: { x: coords.tr.x - cosHalfOffset, y: coords.tr.y + sinHalfOffset } }; coords.bl.corner = { tl: { x: coords.bl.x - sinHalfOffset, y: coords.bl.y - cosHalfOffset }, bl: { x: coords.bl.x - cosHalfOffset, y: coords.bl.y + sinHalfOffset }, br: { x: coords.bl.x + sinHalfOffset, y: coords.bl.y + cosHalfOffset }, tr: { x: coords.bl.x + cosHalfOffset, y: coords.bl.y - sinHalfOffset } }; coords.br.corner = { tr: { x: coords.br.x + cosHalfOffset, y: coords.br.y - sinHalfOffset }, bl: { x: coords.br.x - cosHalfOffset, y: coords.br.y + sinHalfOffset }, br: { x: coords.br.x + sinHalfOffset, y: coords.br.y + cosHalfOffset }, tl: { x: coords.br.x - sinHalfOffset, y: coords.br.y - cosHalfOffset } }; coords.ml.corner = { tl: { x: coords.ml.x - sinHalfOffset, y: coords.ml.y - cosHalfOffset }, tr: { x: coords.ml.x + cosHalfOffset, y: coords.ml.y - sinHalfOffset }, bl: { x: coords.ml.x - cosHalfOffset, y: coords.ml.y + sinHalfOffset }, br: { x: coords.ml.x + sinHalfOffset, y: coords.ml.y + cosHalfOffset } }; coords.mt.corner = { tl: { x: coords.mt.x - sinHalfOffset, y: coords.mt.y - cosHalfOffset }, tr: { x: coords.mt.x + cosHalfOffset, y: coords.mt.y - sinHalfOffset }, bl: { x: coords.mt.x - cosHalfOffset, y: coords.mt.y + sinHalfOffset }, br: { x: coords.mt.x + sinHalfOffset, y: coords.mt.y + cosHalfOffset } }; coords.mr.corner = { tl: { x: coords.mr.x - sinHalfOffset, y: coords.mr.y - cosHalfOffset }, tr: { x: coords.mr.x + cosHalfOffset, y: coords.mr.y - sinHalfOffset }, bl: { x: coords.mr.x - cosHalfOffset, y: coords.mr.y + sinHalfOffset }, br: { x: coords.mr.x + sinHalfOffset, y: coords.mr.y + cosHalfOffset } }; coords.mb.corner = { tl: { x: coords.mb.x - sinHalfOffset, y: coords.mb.y - cosHalfOffset }, tr: { x: coords.mb.x + cosHalfOffset, y: coords.mb.y - sinHalfOffset }, bl: { x: coords.mb.x - cosHalfOffset, y: coords.mb.y + sinHalfOffset }, br: { x: coords.mb.x + sinHalfOffset, y: coords.mb.y + cosHalfOffset } }; coords.mtr.corner = { tl: { x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) }, tr: { x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) }, bl: { x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) }, br: { x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) } }; }, /** * Draws borders of an object's bounding box. * Requires public properties: width, height * Requires public options: padding, borderColor * @method drawBorders * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg * @chainable */ drawBorders: function(ctx) { if (!this.hasBorders) return this; var padding = this.padding, padding2 = padding * 2, strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; ctx.save(); ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = this.borderColor; var scaleX = 1 / this._constrainScale(this.scaleX), scaleY = 1 / this._constrainScale(this.scaleY); ctx.lineWidth = 1 / this.borderScaleFactor; ctx.scale(scaleX, scaleY); var w = this.getWidth(), h = this.getHeight(); ctx.strokeRect( ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, ~~(w + padding2 + strokeWidth * this.scaleX), ~~(h + padding2 + strokeWidth * this.scaleY) ); if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY ? h + (strokeWidth * this.scaleY) + (padding * 2) : -h - (strokeWidth * this.scaleY) - (padding * 2) ) / 2; ctx.beginPath(); ctx.moveTo(0, rotateHeight); ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); ctx.closePath(); ctx.stroke(); } ctx.restore(); return this; }, /** * Draws corners of an object's bounding box. * Requires public properties: width, height, scaleX, scaleY * Requires public options: cornerSize, padding * @method drawControls * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg * @chainable */ drawControls: function(ctx) { if (!this.hasControls) return this; var size = this.cornerSize, size2 = size / 2, strokeWidth2 = this.strokeWidth / 2, left = -(this.width / 2), top = -(this.height / 2), _left, _top, sizeX = size / this.scaleX, sizeY = size / this.scaleY, paddingX = this.padding / this.scaleX, paddingY = this.padding / this.scaleY, scaleOffsetY = size2 / this.scaleY, scaleOffsetX = size2 / this.scaleX, scaleOffsetSizeX = (size2 - size) / this.scaleX, scaleOffsetSizeY = (size2 - size) / this.scaleY, height = this.height, width = this.width, methodName = this.transparentCorners ? 'strokeRect' : 'fillRect', isVML = typeof G_vmlCanvasManager !== 'undefined'; ctx.save(); ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left _left = left - scaleOffsetX - strokeWidth2 - paddingX; _top = top - scaleOffsetY - strokeWidth2 - paddingY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); // top-right _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; _top = top - scaleOffsetY - strokeWidth2 - paddingY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); // bottom-left _left = left - scaleOffsetX - strokeWidth2 - paddingX; _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); // bottom-right _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); if (!this.get('lockUniScaling')) { // middle-top _left = left + width/2 - scaleOffsetX; _top = top - scaleOffsetY - strokeWidth2 - paddingY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); // middle-bottom _left = left + width/2 - scaleOffsetX; _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); // middle-right _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; _top = top + height/2 - scaleOffsetY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); // middle-left _left = left - scaleOffsetX - strokeWidth2 - paddingX; _top = top + height/2 - scaleOffsetY; isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); } // middle-top-rotate if (this.hasRotatingPoint) { _left = left + width/2 - scaleOffsetX; _top = this.flipY ? (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); isVML || ctx.clearRect(_left, _top, sizeX, sizeY); ctx[methodName](_left, _top, sizeX, sizeY); } ctx.restore(); return this; } }); })();