diff --git a/src/control.class.js b/src/control.class.js index 48c9fb66880..bfe5c4dbdcb 100644 --- a/src/control.class.js +++ b/src/control.class.js @@ -86,6 +86,38 @@ */ offsetY: 0, + /** + * Sets the length of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeX: null, + + /** + * Sets the height of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeY: null, + + /** + * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeX: null, + + /** + * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeY: null, + /** * Css cursor style to display when the control is hovered. * if the method `cursorStyleHandler` is provided, this property is ignored. @@ -217,6 +249,66 @@ return point; }, + /** + * Returns the coords for this control based on object values. + * @param {Number} objectAngle angle from the fabric object holding the control + * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if + * isTouch is true) + * @param {Number} centerX x coordinate where the control center should be + * @param {Number} centerY y coordinate where the control center should be + * @param {boolean} isTouch true if touch corner, false if normal corner + */ + calcCornerCoords: function(objectAngle, objectCornerSize, centerX, centerY, isTouch) { + var cosHalfOffset, + sinHalfOffset, + cosHalfOffsetComp, + sinHalfOffsetComp, + xSize = (isTouch) ? this.touchSizeX : this.sizeX, + ySize = (isTouch) ? this.touchSizeY : this.sizeY; + if (xSize && ySize && xSize !== ySize) { + // handle rectangular corners + var controlTriangleAngle = Math.atan2(ySize, xSize); + var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; + var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta); + // use complementary angle for two corners + cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp); + sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp); + } + else { + // handle square corners + // use default object corner size unless size is defined + var cornerSize = (xSize && ySize) ? xSize : objectCornerSize; + /* 0.7071067812 stands for sqrt(2)/2 */ + cornerHypotenuse = cornerSize * 0.7071067812; + // complementary angles are equal since they're both 45 degrees + var newTheta = fabric.util.degreesToRadians(45 - objectAngle); + cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta); + } + + return { + tl: { + x: centerX - sinHalfOffsetComp, + y: centerY - cosHalfOffsetComp, + }, + tr: { + x: centerX + cosHalfOffset, + y: centerY - sinHalfOffset, + }, + bl: { + x: centerX - cosHalfOffset, + y: centerY + sinHalfOffset, + }, + br: { + x: centerX + sinHalfOffsetComp, + y: centerY + cosHalfOffsetComp, + }, + }; + }, + /** * Render function for the control. * When this function runs the context is unscaled. unrotate. Just retina scaled. diff --git a/src/controls.render.js b/src/controls.render.js index 486a5df1a32..d150447fea0 100644 --- a/src/controls.render.js +++ b/src/controls.render.js @@ -19,18 +19,35 @@ */ function renderCircleControl (ctx, left, top, styleOverride, fabricObject) { styleOverride = styleOverride || {}; - var size = styleOverride.cornerSize || fabricObject.cornerSize, + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? styleOverride.transparentCorners : this.transparentCorners, methodName = transparentCorners ? 'stroke' : 'fill', - stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor); + stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor), + myLeft = left, + myTop = top, size; ctx.save(); ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // as soon as fabric react v5, remove ie11, use proper ellipse code. + if (xSize > ySize) { + size = xSize; + ctx.scale(1.0, ySize / xSize); + myTop = top * xSize / ySize; + } + else if (ySize > xSize) { + size = ySize; + ctx.scale(xSize / ySize, 1.0); + myLeft = left * ySize / xSize; + } + else { + size = xSize; + } // this is still wrong ctx.lineWidth = 1; ctx.beginPath(); - ctx.arc(left, top, size / 2, 0, 2 * Math.PI, false); + ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false); ctx[methodName](); if (stroke) { ctx.stroke(); @@ -51,13 +68,14 @@ */ function renderSquareControl(ctx, left, top, styleOverride, fabricObject) { styleOverride = styleOverride || {}; - var size = styleOverride.cornerSize || fabricObject.cornerSize, + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? styleOverride.transparentCorners : fabricObject.transparentCorners, methodName = transparentCorners ? 'stroke' : 'fill', stroke = !transparentCorners && ( styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor - ), sizeBy2 = size / 2; + ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2; ctx.save(); ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; ctx.strokeStyle = styleOverride.strokeCornerColor || fabricObject.strokeCornerColor; @@ -67,10 +85,10 @@ ctx.rotate(degreesToRadians(fabricObject.angle)); // this does not work, and fixed with ( && ) does not make sense. // to have real transparent corners we need the controls on upperCanvas - // transparentCorners || ctx.clearRect(-sizeBy2, -sizeBy2, size, size); - ctx[methodName + 'Rect'](-sizeBy2, -sizeBy2, size, size); + // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize); if (stroke) { - ctx.strokeRect(-sizeBy2, -sizeBy2, size, size); + ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize); } ctx.restore(); } diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index 3170a711898..275a5ea4a22 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -31,8 +31,8 @@ } lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); - // debugging - + // // debugging + // // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); // @@ -73,59 +73,14 @@ * @private */ _setCornerCoords: function() { - var coords = this.oCoords, - newTheta = degreesToRadians(45 - this.angle), - cosTheta = fabric.util.cos(newTheta), - sinTheta = fabric.util.sin(newTheta), - /* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */ - /* 0.707106 stands for sqrt(2)/2 */ - cornerHypotenuse = this.cornerSize * 0.707106, - touchHypotenuse = this.touchCornerSize * 0.707106, - cosHalfOffset = cornerHypotenuse * cosTheta, - sinHalfOffset = cornerHypotenuse * sinTheta, - touchCosHalfOffset = touchHypotenuse * cosTheta, - touchSinHalfOffset = touchHypotenuse * sinTheta, - x, y; + var coords = this.oCoords; for (var control in coords) { - x = coords[control].x; - y = coords[control].y; - coords[control].corner = { - tl: { - x: x - sinHalfOffset, - y: y - cosHalfOffset - }, - tr: { - x: x + cosHalfOffset, - y: y - sinHalfOffset - }, - bl: { - x: x - cosHalfOffset, - y: y + sinHalfOffset - }, - br: { - x: x + sinHalfOffset, - y: y + cosHalfOffset - } - }; - coords[control].touchCorner = { - tl: { - x: x - touchSinHalfOffset, - y: y - touchCosHalfOffset - }, - tr: { - x: x + touchCosHalfOffset, - y: y - touchSinHalfOffset - }, - bl: { - x: x - touchCosHalfOffset, - y: y + touchSinHalfOffset - }, - br: { - x: x + touchSinHalfOffset, - y: y + touchCosHalfOffset - } - }; + var controlObject = this.controls[control]; + coords[control].corner = controlObject.calcCornerCoords( + this.angle, this.cornerSize, coords[control].x, coords[control].y, false); + coords[control].touchCorner = controlObject.calcCornerCoords( + this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true); } }, diff --git a/test/unit/object_interactivity.js b/test/unit/object_interactivity.js index 110fa958df9..6a10ef4ddbc 100644 --- a/test/unit/object_interactivity.js +++ b/test/unit/object_interactivity.js @@ -149,6 +149,62 @@ }); + // set size for bottom left corner and have different results for bl than normal setCornerCoords test + QUnit.test('_setCornerCoords_customControlSize', function(assert) { + //set custom corner size + fabric.Object.prototype.controls.bl.sizeX = 30; + fabric.Object.prototype.controls.bl.sizeY = 10; + + var cObj = new fabric.Object({ top: 10, left: 10, width: 10, height: 10, strokeWidth: 0 }); + assert.ok(typeof cObj._setCornerCoords === 'function', '_setCornerCoords should exist'); + cObj.setCoords(); + + assert.equal(cObj.oCoords.tl.corner.tl.x.toFixed(2), 3.5); + assert.equal(cObj.oCoords.tl.corner.tl.y.toFixed(2), 3.5); + assert.equal(cObj.oCoords.tl.corner.tr.x.toFixed(2), 16.5); + assert.equal(cObj.oCoords.tl.corner.tr.y.toFixed(2), 3.5); + assert.equal(cObj.oCoords.tl.corner.bl.x.toFixed(2), 3.5); + assert.equal(cObj.oCoords.tl.corner.bl.y.toFixed(2), 16.5); + assert.equal(cObj.oCoords.tl.corner.br.x.toFixed(2), 16.5); + assert.equal(cObj.oCoords.tl.corner.br.y.toFixed(2), 16.5); + assert.equal(cObj.oCoords.bl.corner.tl.x.toFixed(2), -5.0); + assert.equal(cObj.oCoords.bl.corner.tl.y.toFixed(2), 15.0); + assert.equal(cObj.oCoords.bl.corner.tr.x.toFixed(2), 25.0); + assert.equal(cObj.oCoords.bl.corner.tr.y.toFixed(2), 15.0); + assert.equal(cObj.oCoords.bl.corner.bl.x.toFixed(2), -5.0); + assert.equal(cObj.oCoords.bl.corner.bl.y.toFixed(2), 25.0); + assert.equal(cObj.oCoords.bl.corner.br.x.toFixed(2), 25.0); + assert.equal(cObj.oCoords.bl.corner.br.y.toFixed(2), 25.0); + assert.equal(cObj.oCoords.tr.corner.tl.x.toFixed(2), 13.5); + assert.equal(cObj.oCoords.tr.corner.tl.y.toFixed(2), 3.5); + assert.equal(cObj.oCoords.tr.corner.tr.x.toFixed(2), 26.5); + assert.equal(cObj.oCoords.tr.corner.tr.y.toFixed(2), 3.5); + assert.equal(cObj.oCoords.tr.corner.bl.x.toFixed(2), 13.5); + assert.equal(cObj.oCoords.tr.corner.bl.y.toFixed(2), 16.5); + assert.equal(cObj.oCoords.tr.corner.br.x.toFixed(2), 26.5); + assert.equal(cObj.oCoords.tr.corner.br.y.toFixed(2), 16.5); + assert.equal(cObj.oCoords.br.corner.tl.x.toFixed(2), 13.5); + assert.equal(cObj.oCoords.br.corner.tl.y.toFixed(2), 13.5); + assert.equal(cObj.oCoords.br.corner.tr.x.toFixed(2), 26.5); + assert.equal(cObj.oCoords.br.corner.tr.y.toFixed(2), 13.5); + assert.equal(cObj.oCoords.br.corner.bl.x.toFixed(2), 13.5); + assert.equal(cObj.oCoords.br.corner.bl.y.toFixed(2), 26.5); + assert.equal(cObj.oCoords.br.corner.br.x.toFixed(2), 26.5); + assert.equal(cObj.oCoords.br.corner.br.y.toFixed(2), 26.5); + assert.equal(cObj.oCoords.mtr.corner.tl.x.toFixed(2), 8.5); + assert.equal(cObj.oCoords.mtr.corner.tl.y.toFixed(2), -36.5); + assert.equal(cObj.oCoords.mtr.corner.tr.x.toFixed(2), 21.5); + assert.equal(cObj.oCoords.mtr.corner.tr.y.toFixed(2), -36.5); + assert.equal(cObj.oCoords.mtr.corner.bl.x.toFixed(2), 8.5); + assert.equal(cObj.oCoords.mtr.corner.bl.y.toFixed(2), -23.5); + assert.equal(cObj.oCoords.mtr.corner.br.x.toFixed(2), 21.5); + assert.equal(cObj.oCoords.mtr.corner.br.y.toFixed(2), -23.5); + + // reset + fabric.Object.prototype.controls.bl.sizeX = null; + fabric.Object.prototype.controls.bl.sizeY = null; + }); + QUnit.test('_findTargetCorner', function(assert) { var cObj = new fabric.Object({ top: 10, left: 10, width: 30, height: 30, strokeWidth: 0 }); assert.ok(typeof cObj._findTargetCorner === 'function', '_findTargetCorner should exist');