I'm working on a problem where I want to enable a user to upload an image which will be displayed inside a pre-defined shape, and any part of the uploaded image outside of the shape will be cut out.
In this example I'm working with a circle, and one of the requirements is for the circle to have a visible border however when I set the globalCompositeOperation: "source-atop"
the border of the circle is also covered by the image. I though that maybe adding another slightly bigger circle around the original circle after the image is added to the canvas might solve the problem but no luck. The code below uses fabricJs
const initBaseCanvas = (imageSize) => {
const container = document.getElementById("customizer-container");
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
// Create base canvas
const initCanvas = new fabric.Canvas("base-image", {
width: containerWidth,
height: containerHeight,
selectable: false,
evented: false,
allowTouchScrolling: true,
backgroundColor: "transparent",
});
// Create the image boundary
const circle = new fabric.Circle({
radius: imageSize / 2,
fill: "",
stroke: "transparent",
backgroundColor: "transparent",
fill: "#f9f9f9",
strokeWidth: 1,
selectable: false,
evented: false,
});
initCanvas.add(circle);
initCanvas.centerObject(circle);
// Insert uploaded image in the center of the circle and pre-select
new fabric.Image.fromURL(
URL.createObjectURL(uploadedFile),
(img) => {
// Scale image down if bigger than canvas to ensure bounding box is visible
const imgWidht = img.width;
if (!imgWidht || imgWidht >= containerWidth) {
img.scaleToWidth(containerWidth - 50);
}
initCanvas.add(img);
initCanvas.centerObject(img);
initCanvas.renderAll();
initCanvas.setActiveObject(img);
const circle2 = new fabric.Circle({
radius: imageSize / 2 + 1,
fill: "transparent",
stroke: "#fd219b",
backgroundColor: "transparent",
strokeWidth: 2,
selectable: false,
evented: false,
globalCompositeOperation: "source-over",
});
initCanvas.add(circle2);
initCanvas.centerObject(circle2);
initCanvas.renderAll();
console.log(initCanvas.getObjects());
},
{
// The image will be clipped outside of the image boundary
globalCompositeOperation: "source-atop",
}
);
return initCanvas;
};
I've tried using all combinations of globalCompositeOperation
as well as adding a secondary circle. I also tried adding a separate canvas with the secondary circle but also no luck.
I've found a solution. Im using a circle as a clip path for the image, and then adding a slightly bigger transparent circle for the border. I ended up not having to use the globalCompositeOperation
thanks to the clipPath
and had to set preserveObjectStacking: true
on the canvas to prevent the image (only selectable component) from jumping to the front of the stack.
/** Initialize base canvas */
const initBaseCanvas = (imageSize) => {
const container = document.getElementById("customizer-container");
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
// Create base canvas
const initCanvas = new fabric.Canvas("base-image", {
width: containerWidth,
height: containerHeight,
selectable: false,
evented: false,
allowTouchScrolling: true,
backgroundColor: "transparent",
preserveObjectStacking: true, // Need this to not bring uploaded image to front when moving
});
// Create the image boundary
const circle = new fabric.Circle({
radius: imageSize / 2,
backgroundColor: "transparent",
fill: "#f9f9f9",
selectable: false,
evented: false,
absolutePositioned: true,
});
initCanvas.add(circle);
initCanvas.centerObject(circle);
// Insert uploaded image in the center of the circle and pre-select
const image = new fabric.Image();
image.clipPath = circle;
initCanvas.add(image);
image.setSrc(URL.createObjectURL(uploadedFile), (img) => {
// Scale image down if bigger than canvas to ensure bounding box is visible
const imgWidht = img.width;
if (!imgWidht || imgWidht >= containerWidth) {
img.scaleToWidth(containerWidth - 50);
}
initCanvas.centerObject(img);
initCanvas.setActiveObject(img);
// Colored border
const circle2 = new fabric.Circle({
radius: imageSize / 2 + 1,
stroke: "#fd219b",
fill: "transparent",
strokeWidth: 2,
selectable: false,
evented: false,
});
initCanvas.add(circle2);
initCanvas.centerObject(circle2);
initCanvas.getObjects()[2].bringToFront();
initCanvas.renderAll();
});
return initCanvas;
};