Reputation: 3039
How can an object be constrained to being resized/scaled only within the bounds of the canvas (or another object in my case) when using Fabric.js?
Currently, I'm handling the object:scaling
event:
// Typescript being used;
onObjectScaling = (event): boolean => {
// Prevent scaling object outside of image
var object = <fabric.IObject>event.target;
var objectBounds: BoundsHelper;
var imageBounds: BoundsHelper;
object.setCoords();
objectBounds = new BoundsHelper(object);
imageBounds = new BoundsHelper(this.imageManipulatorContext.getImageObject());
if (objectBounds.left < imageBounds.left || objectBounds.right > imageBounds.right) {
console.log('horizontal bounds exceeded');
object.scaleX = this.selectedObjectLastScaleX;
object.lockScalingX = true;
object.setCoords();
} else {
this.selectedObjectLastScaleX = object.scaleX;
}
if (objectBounds.top < imageBounds.top || objectBounds.bottom > imageBounds.bottom) {
console.log('vertical bounds exceeded');
object.scaleY = this.selectedObjectLastScaleY;
object.lockScalingY = true;
object.setCoords();
} else {
this.selectedObjectLastScaleY = object.scaleY;
}
return true;
}
**edit The BoundsHelper
class is just a helper for wrapping up the math of calculating the right/bottom sides of the bounding box for an object.
import fabric = require('fabric');
class BoundsHelper {
private assetRect: { left: number; top: number; width: number; height: number; };
get left(): number { return this.assetRect.left; }
get top(): number { return this.assetRect.top; }
get right(): number { return this.assetRect.left + this.assetRect.width; }
get bottom(): number { return this.assetRect.top + this.assetRect.height; }
get height(): number { return this.assetRect.height; }
get width(): number { return this.assetRect.width; }
constructor(asset: fabric.IObject) {
this.assetRect = asset.getBoundingRect();
}
}
export = BoundsHelper;
I also make use of the onBeforeScaleRotate
callback to disable the scaling lock added by the above:
onObjectBeforeScaleRotate = (targetObject: fabric.IObject): boolean => {
targetObject.lockScalingX = targetObject.lockScalingY = false;
return true;
}
The issue I observe is that it seems like Fabric doesn't redraw the object fast enough for me to accurately detect that scale is passing the image's boundary. In other words, if I scale slowly, then life is good; if I scale quickly, then I can scale outside of the image's bounds.
Upvotes: 4
Views: 7022
Reputation: 14741
It is not about speed. Events are discrete sampling of something you are doing with the mouse. Even if you move pixel by pixel virtually, the mouse has its own sample rate and the javascript event do not fire every pixel you move when you go fast.
So if you limit your application to stop scaling when you overcome a limit, when you overcome this limit you will stop your scaling, some pixel after the limit, simply because the last check you were 1 pixel before bound, the event after you are 10 pixel after it.
I changed the code, that is not perfect at all, but gives you idea for dealing with the issue.
When you overcome the limit, instead of lock scaling, calculate the correct scaling to be inside the image and apply that scale.
This logic has problems when you are completely outside the image, so i placed the rect already in, just to demonstrate the different approach.
var BoundsHelper = (function () {
function BoundsHelper(asset) {
this.assetRect = asset.getBoundingRect();
}
Object.defineProperty(BoundsHelper.prototype, "left", {
get: function () {
return this.assetRect.left;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "top", {
get: function () {
return this.assetRect.top;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "right", {
get: function () {
return this.assetRect.left + this.assetRect.width;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "bottom", {
get: function () {
return this.assetRect.top + this.assetRect.height;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "height", {
get: function () {
return this.assetRect.height;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "width", {
get: function () {
return this.assetRect.width;
},
enumerable: true,
configurable: true
});
return BoundsHelper;
})();
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
var canvas = new fabric.Canvas('c');
var rectangle = new fabric.Rect({
fill: 'black',
originX: 'left',
originY: 'top',
stroke: 'false',
opacity: 1,
left: 180,
top: 180,
height: 50,
width: 50
});
var rectangleBounds = new BoundsHelper(rectangle);
var image = new fabric.Image(i, {
selectable: false,
borderColor: 'black',
width: 200,
height: 200
});
canvas.on("object:scaling", function (event) {
var object = event.target;
var objectBounds = null;
var imageBounds = null;
var desiredLength;
object.setCoords();
objectBounds = new BoundsHelper(object);
imageBounds = new BoundsHelper(image);
if (objectBounds.left < imageBounds.left) {
object.lockScalingX = true;
// now i have to calculate the right scaleX factor.
desiredLength =objectBounds.right - imageBounds.left;
object.scaleX = desiredLength / object.width;
}
if (objectBounds.right > imageBounds.right) {
object.lockScalingX = true;
desiredLength = imageBounds.right - objectBounds.left;
object.scaleX = desiredLength / object.width;
}
if (objectBounds.top < imageBounds.top) {
object.lockScalingY = true;
desiredLength = objectBounds.bottom - imageBounds.top;
object.scaleY = desiredLength / object.height;
}
if (objectBounds.bottom > imageBounds.bottom) {
object.lockScalingY = true;
desiredLength = imageBounds.bottom - objectBounds.top;
object.scaleY = desiredLength / object.height;
}
return true;
});
canvas.onBeforeScaleRotate = function (targetObject) {
targetObject.lockScalingX = targetObject.lockScalingY = false;
return true;
};
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = '#555';
var bound = image.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
});
canvas.add(image);
canvas.centerObject(image);
image.setCoords();
canvas.add(rectangle);
canvas.renderAll();
img {
display: none;
}
canvas {
border: solid black 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.12/fabric.min.js"></script>
<img id="i" src="http://fabricjs.com/assets/ladybug.png" />
<canvas id="c" width="500" height="500"></canvas>
https://jsfiddle.net/84zovnek/2/
if (objectBounds.left < imageBounds.left) {
//object.lockScalingX = true;
// now i have to calculate the right scaleX factor.
desiredLength = imageBounds.left - objectBounds.right;
object.scaleX = desiredLength / object.width;
}
Other that, you should update to latest version
Upvotes: 3