Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure mouse corners match custom control corner size #6562

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/control.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this bunch of numbers here i remember they work with square and are a simplified version. Once the object is rotated i believe with a strongly rectangular controls, this does not work.
This same thing overly complicated the padding logic elsewhere.

the x and the y coordiante of each point when rotated are both affected by cos and sin of the 2 offsets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some debug code to uncomment to see those points on the canvas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think I understand the original code now. It's using the complementary angle to triangulate the other corner (which both angles are 45 when in a square, which is why stuff works but it's almost hard-coding to run the original way). Things were working with right angles when I'd rotate so thanks for challenging me to check other rotations.

I'm not sure about debug code though. I did find things below a getImageLines call that I was able to uncomment and see some squares show up so that was hepful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asturur Is there anything else you'd like to see here? I tried adding padding to a shape and the rectangular corner worked, even when rotating.

};
},

/**
* Render function for the control.
* When this function runs the context is unscaled. unrotate. Just retina scaled.
Expand Down
34 changes: 26 additions & 8 deletions src/controls.render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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();
}
Expand Down
61 changes: 8 additions & 53 deletions src/mixins/object_interactivity.mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
//
Expand Down Expand Up @@ -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);
}
},

Expand Down
56 changes: 56 additions & 0 deletions test/unit/object_interactivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down