I have a component that contains a canvas element and that canvas should be the full width of its parent. The canvas itself uses fabricjs and has a background image. The background image actually dictates the height of the canvas because the image has width: 100%
and height: 'auto'
(essentially just an image you can draw on). The canvas and all objects on it should be full responsive because this component can be injected in a variety of locations.
This fiddle is something I found and bookmarked a long time ago, when it scales, the objects scale perfectly. However you’ll notice that the background image does not dictate the canvas size which is a problem.
I tried to make a gif showing the objects jumping but had to fall back on screenshots, apologies. It’s almost as if their coordinate calculations are off, but I’m not sure how to verify.
Image 1: This is where I place the objects on the canvas
Image 2: This is what happens when I begin adjusting the screen width, they scale okay but their origin seems to be offset
Image 3: When I refresh the page on a canvas of a different size their origin appears offset again
The below part is my own code. It deals with the background image/canvas relationship appropriatly, but when I start resizing the container the add objects are a bit off of where they should be and I can’t figure out why. I’ve messed around with settings like top
, left
, originX
, originY
, etc but things are still off.
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const fabricRef = useRef<fabric.Canvas | null>(null);
useEffect(() => {
if (!canvasRef.current) return;
const canvas = initializeFabric({ canvasRef, fabricRef, readOnly });
if (!canvas) return;
const resizeCanvas = () => {
const parent = containerRef.current;
if (parent && fabricRef.current) {
const parentWidth = parent.offsetWidth;
const fabricImage = fabricRef.current.backgroundImage as fabric.Image;
if (fabricImage && fabricImage.width && fabricImage.height) {
const scale = parentWidth / fabricImage.width;
const aspectRatio = fabricImage.width / fabricImage.height;
const calculatedHeight = parentWidth / aspectRatio;
setCanvasHeight(`${calculatedHeight}px`); // Update the canvas height state
// Update the canvas dimensions
fabricRef.current.setDimensions({ width: parentWidth, height: calculatedHeight });
fabricRef.current.calcOffset(); // Refresh the canvas state
// Scale the background image to fit the new dimensions
fabricImage.set({ scaleX: scale, scaleY: scale });
fabricRef.current.setViewportTransform([scale, 0, 0, scale, 0, 0]);
fabricRef.current.renderAll();
}
}
};
const loadImageAndSetAspectRatio = () => {
fabric.Image.fromURL('https://placehold.co/600x400?text=Hello+World', (fabricImage) => {
if (!containerRef?.current) {
console.warn('Container reference is not available.');
return;
}
if (!fabricImage.width || !fabricImage.height) {
console.warn('Image dimensions are not available.');
return;
}
// Obtain the width of the container to which the canvas is attached
const parentWidth = containerRef.current.offsetWidth;
// Calculate the aspect ratio based on the image dimensions
const aspectRatio = fabricImage.width / fabricImage.height;
// Calculate the height needed to maintain this aspect ratio at the given width
const calculatedHeight = parentWidth / aspectRatio;
// Update the state to adjust the canvas height
setCanvasHeight(`${calculatedHeight}px`);
if (fabricRef.current) {
// Set the canvas dimensions to match the container
fabricRef.current.setDimensions({
width: parentWidth,
height: calculatedHeight,
});
fabricRef.current.calcOffset(); // Refresh the canvas state
// Set the background image with scaling to fit the new dimensions
fabricRef.current.setBackgroundImage(
fabricImage,
fabricRef.current.renderAll.bind(fabricRef.current),
{
originX: 'left',
originY: 'top',
scaleX: parentWidth / fabricImage.width,
scaleY: calculatedHeight / fabricImage.height,
}
);
}
});
};
const loadCanvasObjects = () => {
if (!fabricRef.current) return;
const jsonObjects = serverData.map((x: any) => ({ ...x.fabricJsonData }));
jsonObjects.forEach((objData) => {
fabric.util.enlivenObjects(
[objData],
(objects: any[]) => {
objects.forEach((obj) => {
fabricRef.current!.add(obj);
});
fabricRef.current!.renderAll();
},
'',
undefined
);
});
};
// Load image and set up canvas size
loadImageAndSetAspectRatio();
loadCanvasObjects();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
fabricRef?.current?.dispose();
};
}, [backgroundImage, containerRef]);
<Box
ref={containerRef}
sx={{
display: 'block',
width: '100%',
height: canvasHeight,
overflow: 'hidden',
border: 'solid 1px red',
}}
>
<canvas id='canvas' ref={canvasRef} style={{ display: 'block', border: 'solid 1px blue' }} />;
</Box>
Here’s the code I use when placing a new object
export const createCanvasCircle = (note: any) => {
const { x, y, color, number, canEdit } = note;
const circle = new fabric.Circle({
radius: 15,
fill: color,
left: x,
top: y,
originX: 'center',
originY: 'center',
});
const textLabel = new fabric.Text(String(number), {
fontSize: 12,
fill: 'white',
left: x,
top: y,
originX: 'center',
originY: 'center',
fontFamily: 'Arial',
});
const group = new fabric.Group([circle, textLabel], {
left: x,
top: y,
originX: 'center',
originY: 'center',
selectable: canEdit,
hasControls: canEdit,
hasBorders: canEdit,
lockMovementX: !canEdit,
lockMovementY: !canEdit,
lockScalingX: !canEdit,
lockScalingY: !canEdit,
lockRotation: !canEdit,
});
return group
};
Sorry for the long post, I’ve been stuck on this for over a week. If you can make my code work so the objects coordinates don’t jump around or the fiddle code work so that the background image scales and dictates the canvas size I would be ecstatic. I should be able to piece the rest together.