How to create a canvas with a limited drawing area on a background image in Fabric.js


Introduction
SpaceRunners has a design tool where artists can create custom designs in a limited drawing area on any physical object. For example, the SpaceRunners team can upload an image of a T-Shirt and set it as a template. Inside this T-Shirt you can only design on a predefined area as shown in the image below:
This image shows a part of the SpaceRunners admin where one can position the drawing area inside the template and set its width and height. This area makes sense in the context of bringing these designs to the real world, for example printing the T-Shirt. Design elements that go outside of this area will be cut off when the design is exported.
On the frontend, SpaceRunners is using Fabric.js to power its design tool. Fabric.js provides an object model on top of the HTML canvas element to help build canvas experiences faster, with less code and in a more maintainable way. More information can be found in their docs. This article will describe a very well designed UX flow for how to achieve the requirements explained above and also how it can be done with Fabric.js. There's no straightforward API for this and figuring it out took some time so we hope this article will be helpful to others.
UI considerations to create a seamless user experience
The canvas has to span the full width and height of its container inside the editor. In the image below the canvas is the entire area in gray, below the editor tools and next to the image generation tools.
This allows for two things:
When an object on the canvas goes outside of the drawing area although the object itself is cut off, the resize and rotate controls should still be visible. Since those controls are canvas based everything around the drawing area also has to be a canvas
The canvas can be zoomed in and out where the entire background object and the design elements zoom together.
An easy to implement approach would be to have the background image as an HTML element and then absolutely position the drawing area on it using the coordinates from the admin. In this case only the drawing area would be the canvas. Looking at this approach through the lens of the two requirements above, this would mean that controls wouldn't be visible outside of the drawing area and the background image wouldn't zoom together with the canvas.
The approach to achieve the desired result
Set a background image on the canvas using its
setBackgroundImage
function. This makes the background image a part of the canvas and can be zoomed in and out together with everything elseAdd a Fabric.js clip path to the canvas. A Fabric.js canvas object has a
clipPath
property where any shape can be place. When objects on the canvas go outside of the clip path they're clipped.Add an overlay image to the canvas using the
setOverlayImage
from Fabric.jsSet an inverted clip path to this overlay image. An inverted clip path cuts off everything except the shape it has been defined for it. The reason for this inverted clip path is to prevent the original clip path from also cutting off the background image.
Here's the code:
export const setBackgroundImage = (canvas, img, isMobile) => {
// The clipPath object here has been previously set based on the settings for the template
const { clipPath, width, height, wrapperEl } = canvas;
// This calculates the background image width to fit inside the container
const { clientHeight, clientWidth } = wrapperEl;
const aspectRatio = clientWidth / clientHeight;
const imageWidth =
(canvas.width *
(isMobile
? GARMENT_IMAGE_MOBILE_WIDTH
: clientHeight * PERCENTAGE_OF_CONTAINER_HEIGHT * aspectRatio)) /
clientWidth;
img.scaleToWidth(imageWidth);
img.set({
left: width / 2,
top: height / 2,
originX: 'center',
originY: 'center',
selectable: false,
centeredScaling: true,
erasable: false,
excludeFromExport: true,
});
canvas.renderAll();
const oldClipPath = { ...clipPath };
canvas.clipPath.top = clipPath.templateBasedTop;
// This positions the clip path based on current container size because the canvas is responsive to container dimensions changes
scaleObjectTops(canvas, oldClipPath);
canvas.setBackgroundImage(img).renderAll();
// This adds the overlay background image which prevents the original background image from being clipped by the clip path
img.clone((copy) => {
const clipPath2 = new fabric.Rect({
height: clipPath.height - 2,
selectable: false,
stroke: 'transparent',
strokeWidth: 0,
width: clipPath.width - 2,
left: clipPath.left + 1,
top: clipPath.top + 1,
inverted: true,
absolutePositioned: true,
excludeFromExport: true,
});
copy.set({
clipPath: clipPath2,
});
canvas.setOverlayImage(copy).renderAll();
});
};
Here’s the end result visually:
Conclusion
The code for your app will look different depending on how complex you want your experience to be, but you can reuse the code provided here to achieve this particular pattern with minor adjustments.
Feel free to go try it out at ablo.ai. We’re currently offering 1000 free credits to use our AI design tools.
In future articles we'll explain more Fabric.js concepts and how to achieve other complex behaviors.
Subscribe to my newsletter
Read articles from Mihovil Kovacevic directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
