Skip to content

Commit

Permalink
feat(fabric.Control) Add custom control size per control.
Browse files Browse the repository at this point in the history
  • Loading branch information
gloriousjob authored Oct 11, 2020
1 parent 7de3b1f commit d4f672a
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 61 deletions.
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,
},
};
},

/**
* 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

0 comments on commit d4f672a

Please sign in to comment.