This can be accomplished with Fabric using the clipTo
property, but you have to 'reverse' the transformations (scale and rotation), in the clipTo
function.
When you use the clipTo
property in Fabric, the scaling and rotation are applied after the clipping, which means that the clipping is scaled and rotated with the image. You have to counter this by applying the exact reverse of the transformations in the clipTo
property function.
My solution involves having a Fabric.Rect
serve as the 'placeholder' for the clip region (this has advantages because you can use Fabric to move the object around and thus the clip region.
Please note that my solution uses the Lo-Dash utility library, particularly for _.bind()
(see code for context).
Breakdown
1. Initialize Fabric
First, we want our canvas, of course:
var canvas = new fabric.Canvas('c');
2. Clip Region
var clipRect1 = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 180,
top: 10,
width: 200,
height: 200,
fill: 'none',
stroke: 'black',
strokeWidth: 2,
selectable: false
});
We give these Rect
objects a name property, clipFor
, so the clipTo
functions can find the one by which they want to be clipped:
clipRect1.set({
clipFor: 'pug'
});
canvas.add(clipRect1);
There doesn't have to be an actual object for the clip region, but it makes it easier to manage, as you're able to move it around using Fabric.
3. Clipping Function
We define the function which will be used by the images' clipTo
properties separately to avoid code duplication:
Since the angle
property of the Image object is stored in degrees, we'll use this to convert it to radians.
function degToRad(degrees) {
return degrees * (Math.PI / 180);
}
findByClipName()
is a convenience function, which is using Lo-Dash, to find the with the clipFor
property for the Image object to be clipped (for example, in the image below, name
will be 'pug'
):
function findByClipName(name) {
return _(canvas.getObjects()).where({
clipFor: name
}).first()
}
And this is the part that does the work:
var clipByName = function (ctx) {
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
ctx.translate(0,0);
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
ctx.rect(
clipRect.left - this.left,
clipRect.top - this.top,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
}
NOTE: See below for an explanation of the use of this
in the function above.
4. fabric.Image
object using clipByName()
Finally, the image can be instantiated and made to use the clipByName
function like this:
var pugImg = new Image();
pugImg.onload = function (img) {
var pug = new fabric.Image(pugImg, {
angle: 45,
width: 500,
height: 500,
left: 230,
top: 170,
scaleX: 0.3,
scaleY: 0.3,
clipName: 'pug',
clipTo: function(ctx) {
return _.bind(clipByName, pug)(ctx)
}
});
canvas.add(pug);
};
pugImg.src = 'https://fabricjs.com/lib/pug.jpg';
What does _.bind()
do?
Note that the reference is wrapped in the _.bind()
function.
I'm using _.bind()
for the following two reasons:
- We need to pass a reference
Image
object to clipByName()
- The
clipTo
property is passed the canvas context, not the object.
Basically, _.bind()
lets you create a version of the function that uses the object you specify as the this
context.
Sources
- https://lodash.com/docs#bind
- https://fabricjs.com/docs/fabric.Object.html#clipTo
- https://html5.litten.com/understanding-save-and-restore-for-the-canvas-context/