Master Fabric.js: Build Interactive Canvas with Zoom, Snap, and Guides
This article walks through setting up Fabric.js on a web page, creating a canvas, drawing rectangles, adding zoom and pan controls, implementing object snapping and alignment guides, and provides complete source code so readers can quickly build an interactive canvas application.
1. Initialize Fabric Canvas Instance
To start, add an HTML5 <canvas> element to the page and create a fabric.Canvas instance in index.js with the option preserveObjectStacking: true so that object stacking order is retained.
<!DOCTYPE html>
<html>
<head>
<title>Fabric - Canvas</title>
<meta charset="UTF-8" />
</head>
<body>
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;">
<div style="display: flex; align-items: center; gap: 4px;">
Panning: <button id="btn-panning">Start</button>
</div>
<div id="app" style="display: flex; align-items: center; justify-content: center;">
<canvas id="c" width="600" height="600"></canvas>
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>In index.js the canvas is instantiated as follows:
import { fabric } from "fabric";
const canvas = (window._canvas = new fabric.Canvas("c", { preserveObjectStacking: true }));2. Draw Rectangle Objects
Event bindings are set up for mouse actions, and a helper createRect creates rectangles with optional fill color.
const bindEvents = () => {
canvas.on('mouse:up', onMouseUp);
canvas.on('mouse:down', onMouseDown);
canvas.on('mouse:move', onMouseMove);
canvas.on('mouse:wheel', onMouseWheel);
canvas.on('zoom:changed', onZoomChanged);
canvas.on('object:moving', onObjectMoving);
canvas.on('selection:cleared', onSelectionCleared);
};
const createRect = (left, top, color, width, height) => {
const options = {
originX: "left",
originY: "top",
left,
top,
width,
height,
};
if (color) {
options["fill"] = color;
}
const rect = new fabric.Rect(options);
rect.guides = {};
rect.guidePoints = {};
canvas.add(rect);
canvas.renderAll();
};
const init = () => {
bindEvents();
createRect(200, 200, "rgba(0, 0, 255, 1)", getRandomNumber(100, 200), getRandomNumber(100, 200));
createRect(Math.floor(Math.random() * canvas.width), Math.floor(Math.random() * canvas.height), "rgba(0, 255, 0, 1)", getRandomNumber(100, 200), getRandomNumber(100, 200));
createRect(Math.floor(Math.random() * canvas.width), Math.floor(Math.random() * canvas.height), "rgba(255, 0, 0, 1)", getRandomNumber(100, 200), getRandomNumber(100, 200));
window.addEventListener('wheel', preventBrowserZoom, { passive: false });
};
init();3. Add Zoom Functionality
When the user holds Ctrl and scrolls the mouse wheel, the canvas zoom level changes. The onMouseWheel handler adjusts the zoom, clamps it between 0.3 and 30, and calls canvas.zoomToPoint. If only the wheel is used, the canvas is panned.
const onMouseWheel = (opt) => {
if (opt.e.ctrlKey) {
const delta = opt.e.deltaY;
let zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
if (zoom > 30) zoom = 30;
if (zoom < 0.3) zoom = 0.3;
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
} else {
const e = opt.e;
const currentViewportTransform = canvas.viewportTransform;
currentViewportTransform[4] -= e.deltaX;
currentViewportTransform[5] -= e.deltaY;
canvas.forEachObject((obj) => obj.setCoords());
canvas.requestRenderAll();
opt.e.preventDefault();
opt.e.stopPropagation();
}
};4. Add Object Snap and Alignment Guides
When a rectangle is moved, the object:moving event triggers snapObject and drawGuides. snapObject finds the nearest vertical and horizontal edges of other objects and, if within a threshold, aligns the moving object. drawGuides draws temporary lines and small marks to visualise the alignment.
const onObjectMoving = (e) => {
const obj = e.target;
if (!(obj instanceof fabric.Rect)) return false;
obj.set("left", Math.round(obj.left));
obj.set("top", Math.round(obj.top));
if (isPanning) return;
snapObject(obj);
drawGuides(obj);
};
const snapObject = (obj) => {
let objVerticals = [obj.left, obj.left + Math.round(obj.getScaledWidth()), obj.left + Math.round(obj.getScaledWidth()) / 2.0];
let objHorizontals = [obj.top, obj.top + Math.round(obj.getScaledHeight()), obj.top + Math.round(obj.getScaledHeight()) / 2.0];
const targets = canvas.getObjects().filter(o => o.type !== "line" && o !== obj);
let minAbsDiffVertical = 999;
let minAbsDiffHorizontal = 999;
let newPosVertical = 0;
let newPosHorizontal = 0;
for (const target of targets) {
const targetVerticals = [target.left, target.left + Math.round(target.getScaledWidth()), target.left + Math.round(target.getScaledWidth()) / 2.0];
const targetHorizontals = [target.top, target.top + Math.round(target.getScaledHeight()), target.top + Math.round(target.getScaledHeight()) / 2.0];
targetVerticals.forEach((targetVertical) => {
objVerticals.forEach((objVertical) => {
if (Math.abs(targetVertical - objVertical) < minAbsDiffVertical) {
minAbsDiffVertical = Math.abs(targetVertical - objVertical);
newPosVertical = obj.left + targetVertical - objVertical;
}
});
});
targetHorizontals.forEach((targetHorizontal) => {
objHorizontals.forEach((objHorizontal) => {
if (Math.abs(targetHorizontal - objHorizontal) < minAbsDiffHorizontal) {
minAbsDiffHorizontal = Math.abs(targetHorizontal - objHorizontal);
newPosHorizontal = obj.top + targetHorizontal - objHorizontal;
}
});
});
}
if (minAbsDiffHorizontal < 85) obj.set("top", newPosHorizontal);
if (minAbsDiffVertical < 85) obj.set("left", newPosVertical);
obj.setCoords();
};
const drawGuides = (obj) => {
const targets = canvas.getObjects().filter(o => o.type !== "line" && o !== obj);
const objLeft = obj.left;
const objRight = obj.left + Math.round(obj.getScaledWidth());
const objCenterX = (objLeft + objRight) / 2.0;
const objTop = obj.top;
const objBottom = obj.top + Math.round(obj.getScaledHeight());
const objCenterY = (objTop + objBottom) / 2.0;
const sides = ["left", "right", "centerX", "top", "bottom", "centerY"];
sides.forEach((side) => {
let value;
let pointArray = [];
switch (side) {
case "top":
value = objTop;
pointArray = [objLeft, objRight, objCenterX];
break;
case "bottom":
value = objBottom;
pointArray = [objLeft, objRight, objCenterX];
break;
case "centerY":
value = objCenterY;
pointArray = [objLeft, objRight, objCenterX];
break;
case "left":
value = objLeft;
pointArray = [objTop, objBottom, objCenterY];
break;
case "right":
value = objRight;
pointArray = [objTop, objBottom, objCenterY];
break;
case "centerX":
value = objCenterX;
pointArray = [objTop, objBottom, objCenterY];
break;
}
for (const target of targets) {
const targetLeft = target.left;
const targetRight = target.left + Math.round(target.getScaledWidth());
const targetCenterX = (targetLeft + targetRight) / 2.0;
const targetTop = target.top;
const targetBottom = target.top + Math.round(target.getScaledHeight());
const targetCenterY = (targetTop + targetBottom) / 2.0;
switch (side) {
case "top":
case "bottom":
case "centerY":
if (inRange(value, targetTop) || inRange(value, targetBottom) || inRange(value, targetCenterY)) {
pointArray.push(targetLeft, targetRight, targetCenterX);
}
break;
case "left":
case "right":
case "centerX":
if (inRange(value, targetLeft) || inRange(value, targetRight) || inRange(value, targetCenterX)) {
pointArray.push(targetBottom, targetTop, targetCenterY);
}
break;
}
}
if (obj.guides[side] instanceof fabric.Line) {
canvas.remove(obj.guides[side]);
delete obj.guides[side];
}
if (obj.guidePoints[side] != null) {
obj.guidePoints[side].forEach((mark) => canvas.remove(mark));
delete obj.guidePoints[side];
}
if (pointArray.length <= 3) return;
const sortedPointArray = pointArray.sort((a, b) => a - b);
const lineProps = { evented: true, stroke: "black", strokeWidth: 1, selectable: false, opacity: 1 };
const MIN_SIZE = 5;
const MIN_ZOOM = 0.3;
const MAX_ZOOM = 30;
const zoom = canvas.getZoom();
const limitedZoomNumber = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
const markSize = Math.max(MIN_SIZE, Math.round(MIN_SIZE / limitedZoomNumber));
let ln;
let marks = [];
if (side === "top" || side === "bottom" || side === "centerY") {
ln = new fabric.Line([sortedPointArray[0], value, sortedPointArray[sortedPointArray.length - 1], value], Object.assign(lineProps, { strokeWidth: lineProps.strokeWidth / zoom }));
sortedPointArray.forEach((point) => {
marks.push(new fabric.Line([point - markSize, value - markSize, point + markSize, value + markSize], Object.assign(lineProps, {})));
marks.push(new fabric.Line([point - markSize, value + markSize, point + markSize, value - markSize], Object.assign(lineProps, {})));
});
} else {
ln = new fabric.Line([value, sortedPointArray[0], value, sortedPointArray[sortedPointArray.length - 1]], Object.assign(lineProps, { strokeWidth: lineProps.strokeWidth / zoom }));
sortedPointArray.forEach((point) => {
marks.push(new fabric.Line([value - markSize, point - markSize, value + markSize, point + markSize], Object.assign(lineProps, {})));
marks.push(new fabric.Line([value - markSize, point + markSize, value + markSize, point - markSize], Object.assign(lineProps, {})));
});
}
obj.guides[side] = ln;
obj.guidePoints[side] = marks;
canvas.add(ln);
marks.forEach((mark) => canvas.add(mark));
canvas.renderAll();
});
};
const inRange = (a, b) => a === b;5. Implement Mouse‑Based Panning
A button toggles a boolean isPanning. When active and the left mouse button is pressed, mouse movements translate the canvas viewport.
const button = document.getElementById("btn-panning");
const handleButtonClick = () => {
if (isPanning == true) {
isPanning = false;
button.innerHTML = "Start";
} else {
isPanning = true;
button.innerHTML = "End";
}
};
const onMouseDown = (opt) => {
if (opt?.button == 1 && isPanning == true) {
startPanning = true;
lastPosX = opt?.e?.clientX;
lastPosY = opt?.e?.clientY;
}
};
const onMouseMove = (opt) => {
if (startPanning) {
const currentViewportTransform = canvas.viewportTransform;
if (currentViewportTransform) {
currentViewportTransform[4] += opt?.e?.clientX - lastPosX;
currentViewportTransform[5] += opt?.e?.clientY - lastPosY;
canvas.requestRenderAll();
lastPosX = opt?.e?.clientX;
lastPosY = opt?.e?.clientY;
}
}
};6. Full Source Code
The complete HTML and JavaScript files are provided above; running them creates a canvas with three random rectangles, zoom‑and‑pan controls, and dynamic alignment guides.
Conclusion
This article shares practical experience using Fabric.js for canvas manipulation, covering initialization, drawing, zooming, snapping, guide rendering, and panning, with full source code to help developers get started quickly.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
