This blog was written in November 13, 2022
In my previous job, I was working on an image SASS tool that collects data for machine learning by data collectors.
They upload images and mark a part of an image with shapes such as rectangles or polygons. Then label on them.
The tool generates the data as a JSON, and that is used for machine learning models.
The other day, I took on a feature of my team project that is similar to the tool I mentioned. I was creating features with svg
and HTML Elements. Without library. But since there is a short deadline at this time, I've decided to use a library for the feature.
Must
- A library must be free to use for the commercial use
- A library must continue to be maintained
Need to implement
- Draw Shapes (rects, ellipses)
- Draw and Edit Text
- Move and Resize objects by mouse
- Rotate Image
- Download Canvas as an Image
- Import and Export in JSON
- Prevent Editing a part of objects
I listed up what I needed. Then, I googled and found two libraries that looked fit on our project.
Libraries
- Fabric.js - is a powerful and simple Javascrript HTML5 canvas library
- Konvajs - is an HTML5 Canvas JavaScript framework that enables high performance animations, transitions, node nesting, layering, filtering, caching, event handling for desktop and mobile applications, and much more.
Fabric.js
is a library that the colleague also recommended. In the home page, I got to see every features right away that I needed. It already looked perfect for me though, I thought it would be good to search for other alternatives.
Konvajs
has many features. It provides a library(react-konva) for React. Since my team were working with React, it seemed a great option.
Of course, besides these two, there are many good and fantastic canvas libraries. There's a time limit and I wanted to try a quick, so, I chose two libraries.
Point of Comparison
- Popularity
- A Number of Github Stars
- A Number of Used by
- A Number of Contributors
- npm trends - Downloads in past 1 year
- Stable
- Current Version of The Library
- A Number of Issues
- Contribution Period
- Easy to use
- Documentation
- Fit with React
- Learning Curve
In the easy to use section, I will make quick examples with them and will review briefly. Before starting it, you should know. it's just my opinion for my project. Your perspective can be different depending on what you want.
Popularity
A Number of Github Stars
Fabric.js | Konva | React Konva |
---|---|---|
23,2k | 8.4k | 4.9k |
A Number of Used by
Fabric.js | Konva | React Konva |
---|---|---|
7.5k | 20.7k | 15.9k |
A Number of Contributors
Fabric.js | Konva | React Konva |
---|---|---|
277 | 158 | 27 |
npm trends - Downloads in past 1 year
Konvajs
has the library for React, and its download number is also be in high-demand.
Stable
Version and A Number of Issues
Fabric.js | Konva | React Konva | |
---|---|---|---|
version | 5.2.4 | 2.1.3 | - |
open issues | 241 | 56 | 19 |
closed issues | 5,327 | 1,101 | 589 |
Contribution Period
- Fabric.js
- Konvajs
- react-konvajs
It's been so long since they came out, I think both libraries are mature enough. It's no doubt.
Easy to use
I've implemented the features that I mentioned in need to implement
with each library on the page. I will show you with the code.
Draw Shapes
Fabricjs
...
export function createRect(
x: number,
y: number,
penColor: PenColor,
readonly = false
): fabric.Rect {
const rect = new fabric.Rect({
left: x,
top: y,
originX: "left",
originY: "top",
width: 0,
height: 0,
fill: getRGBFromPenColor(penColor, 0.3),
stroke: getRGBFromPenColor(penColor),
borderColor: "white",
strokeWidth: 1,
selectable: !readonly,
});
rect.set("strokeUniform", true);
rect.setControlsVisibility({ mtr: false });
return rect;
}
...
rect = createRect(origX, origY, penColor, readonly);
canvas.add(rect);
...
Konvajs
...
<Rect
draggable={selected}
ref={(node) => {
setNode(node);
}}
x={shape.x}
y={shape.y}
width={shape.width}
height={shape.height}
fill={shape.fillColor}
stroke={shape.strokeColor}
strokeWidth={1}
onClick={onSelect}
onDragEnd={handleDragEnd}
onTransformEnd={handleTransformEnd}
/>
...
Fabricjs
: Create an instance using fabric.Rect and add it into canvas.
Konvajs
: Render Rect Component
Both ways are really easy. but in Fabricjs
we have to manage instances that are created by fabric
, so, I would prefer Konvajs
more than Fabricjs
.
Draw and Edit Text
Fabricjs
...
export function createText(
x: number,
y: number,
fontSize: number,
penColor: PenColor,
readonly = false
): fabric.Textbox {
const text = new fabric.Textbox("Text", {
left: x,
top: y,
originX: "left",
originY: "top",
fill: getRGBFromPenColor(penColor),
width: 0,
height: 0,
selectable: !readonly,
fontSize,
lockScalingY: true,
});
text.set({ strokeUniform: true });
text.setControlsVisibility({
mtr: false,
tl: false,
tr: false,
bl: false,
br: false,
mb: false,
mt: false,
});
return text;
}
...
text = createText(
origX,
origY,
drawMode === "TEXT_S" ? 16 : 32,
penColor,
readonly
);
canvas.add(text);
...
Konvajs
...
<Text
draggable={selected}
ref={(node) => {
setNode(node);
}}
x={shape.x}
y={shape.y}
width={shape.width}
color={shape.color}
text={shape.text}
fontSize={shape.fontSize}
onClick={onSelect}
onDragEnd={handleDragEnd}
onTransform={handleTransformEnd}
/>
...
const handleTextInput = useCallback((index: number, char: string) => {
setShapes((prevShapes) => {
const newShapes = [...prevShapes];
const target = newShapes[index];
if (isPaintText(target)) {
if (char === "Backspace") {
target.text = target.text.substring(0, target.text.length - 1);
} else {
target.text += char;
}
}
return newShapes;
});
}, []);
...
useEffect(() => {
if (!selectedShape) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Enter") {
setSelectedShape(undefined);
return;
}
onTextInput?.(selectedShape.index, e.key);
};
window.addEventListener("keydown", handleKey);
return () => {
window.removeEventListener("keydown", handleKey);
};
}, [onTextInput, selectedShape]);
...
Fabricjs
: It handles text changing by default. You can click and edit text
Konvajs
: To edit text, we have to provide the interface for changing text our selves. It was a little hassle. I had to finish this research as soon as possible, so, I just added keydown
event listener to window
and changed text in there.
Move and Resize objects by mouse
Fabricjs
...
useEffect(() => {
if (!canvas) return;
let drawing = false,
origX = 0,
origY = 0,
rect: fabric.Rect | undefined = undefined,
ellipse: fabric.Ellipse | undefined = undefined,
text: fabric.Textbox | undefined = undefined;
const handleCanvasMouseDown = (o: fabric.IEvent<MouseEvent>) => {
if (paintInfo.current.drawMode === "SELECT") return;
drawing = true;
const pointer = canvas.getPointer(o.e);
origX = pointer.x;
origY = pointer.y;
canvas.renderAll();
};
const handleCanvasMouseMove = (o: fabric.IEvent<MouseEvent>) => {
if (!drawing) return;
const pointer = canvas.getPointer(o.e);
if (arePointsOutOfRange(pointer.x, pointer.y)) return;
switch (paintInfo.current.drawMode) {
case "RECT":
if (rect) resizeRect(rect, origX, origY, pointer.x, pointer.y);
break;
case "ELLIPSE":
if (ellipse)
resizeEllipse(ellipse, origX, origY, pointer.x, pointer.y);
break;
case "TEXT_S":
case "TEXT_L":
if (text) resizeText(text, origX, pointer.x);
break;
}
canvas.renderAll();
};
const handleCanvasMouseUp = (o: fabric.IEvent<MouseEvent>) => {
drawing = false;
paintInfo.current.onDrawEnd?.();
canvas.renderAll();
};
canvas.on("mouse:down", handleCanvasMouseDown);
canvas.on("mouse:move", handleCanvasMouseMove);
canvas.on("mouse:up", handleCanvasMouseUp);
return () => {
canvas.off("mouse:down");
canvas.off("mouse:move");
canvas.off("mouse:up");
};
...
export function resizeRect(
rect: fabric.Rect,
origX: number,
origY: number,
x: number,
y: number
) {
if (x < origX) rect.set({ left: x });
if (y < origY) rect.set({ top: y });
rect.set({
width: Math.abs(origX - x),
height: Math.abs(origY - y),
});
}
export function resizeEllipse(
ellipse: fabric.Ellipse,
origX: number,
origY: number,
x: number,
y: number
) {
if (x < origX) ellipse.set({ left: x });
if (y < origY) ellipse.set({ top: y });
ellipse.set({ rx: Math.abs(origX - x) / 2, ry: Math.abs(origY - y) / 2 });
}
export function resizeText(text: fabric.Textbox, origX: number, x: number) {
if (x < origX) text.set({ left: x });
text.set({
width: Math.abs(origX - x),
});
}
...
Konvajs
...
const Shape = ({
shape,
selected,
border,
onSelect,
onMoveEnd,
onResizeEnd,
}: {
shape?: PaintShape;
selected?: boolean;
border?: boolean;
onSelect?: VoidFunction;
onMoveEnd?: (pos: Position) => void;
onResizeEnd?: (size: Size) => void;
}) => {
const [node, setNode] = useState<
Konva.Rect | Konva.Ellipse | Konva.Text | null
>();
const trRef = React.useRef<Konva.Transformer>(null);
const shapeComp = useMemo(() => {
if (!shape) return null;
const handleDragEnd = (e: KonvaEventObject<DragEvent>) => {
onMoveEnd?.({ x: e.target.x(), y: e.target.y() });
};
const handleTransformEnd = (e: KonvaEventObject<DragEvent>) => {
const node = e.target;
const [scaleX, scaleY, nodeWidth, nodeHeight] = [
node.scaleX(),
node.scaleY(),
node.width(),
node.height(),
];
node.scaleX(1);
node.scaleY(1);
const [width, height] = [scaleX * nodeWidth, scaleY * nodeHeight];
onResizeEnd?.({ width, height });
};
if (isPaintRect(shape)) {
return (
<Rect
draggable={selected}
ref={(node) => {
setNode(node);
}}
x={shape.x}
y={shape.y}
width={shape.width}
height={shape.height}
fill={shape.fillColor}
stroke={shape.strokeColor}
strokeWidth={1}
onClick={onSelect}
onDragEnd={handleDragEnd}
onTransformEnd={handleTransformEnd}
/>
);
} else if (isPaintEllipse(shape)) {
return (
<Ellipse
draggable={selected}
ref={(node) => {
setNode(node);
}}
x={shape.x}
y={shape.y}
radiusX={shape.radiusX}
radiusY={shape.radiusY}
fill={shape.fillColor}
stroke={shape.strokeColor}
strokeWidth={1}
onClick={onSelect}
onDragEnd={handleDragEnd}
onTransformEnd={handleTransformEnd}
/>
);
} else if (isPaintText(shape)) {
return (
<Text
draggable={selected}
ref={(node) => {
setNode(node);
}}
x={shape.x}
y={shape.y}
width={shape.width}
color={shape.color}
text={shape.text}
fontSize={shape.fontSize}
onClick={onSelect}
onDragEnd={handleDragEnd}
onTransform={handleTransformEnd}
/>
);
}
return null;
}, [onMoveEnd, onResizeEnd, onSelect, shape, selected]);
useEffect(() => {
if (!trRef.current || !node || !selected) return;
trRef.current.nodes([node]);
trRef.current.getLayer()?.batchDraw();
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
trRef.current?.detach();
};
}, [node, selected]);
return (
<>
{shapeComp}
<Transformer
ref={trRef}
enabledAnchors={
shape && isPaintText(shape)
? ["middle-left", "middle-right"]
: undefined
}
rotateEnabled={false}
/>
{border && shape && isPaintText(shape) && (
<Rect
{...shape}
height={shape.fontSize}
stroke="black"
strokeWidth={1}
/>
)}
</>
);
};
...
Fabricjs
: It provides moving and resizing itself. There was only what I had to do is just changing position and size in mouse events.
Konvajs
: There were more things that I had to do than I did in Fabricjs
. For resizing, I had to attach Transfomer
component to a shape component.
Rotate Image
Fabricjs
const rotateBackgroundImage = () => {
if (!canvas?.backgroundImage) return;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const backgroundImage: any = canvas.backgroundImage;
backgroundImage.rotate((backgroundImage.angle + 90) % 360);
canvas.renderAll();
};
Konvajs
...
const rotateBackgroundImage = () => {
setBgImgRotation((prevRotation) => (prevRotation + 90) % 360);
};
...
<Layer>
<Image
image={bgImg}
offsetX={bgImg.width / 2}
offsetY={bgImg.height / 2}
x={bgImg.width / 2}
y={bgImg.height / 2}
rotation={bgImgRotation}
/>
</Layer>
...
Fabricjs
: It has a background image field in canvas. I got to use it to rotate the background.
Konvajs
: I rendered a Image
Component and I used the props to rotate.
Download Canvas as an Image
Fabricjs
const downloadCanvasAsImage = () => {
if (!canvas) return;
const a = document.createElement("a");
a.href = canvas.toDataURL({
format: "jpeg",
quality: 0.8,
});
a.download = "canvas.jpeg";
a.click();
a.remove();
};
Konvajs
const downloadCanvasAsImage = () => {
if (!stageEl.current) return;
const a = document.createElement("a");
a.href = stageEl.current.toDataURL({
quality: 0.8,
});
a.download = "canvas.jpeg";
a.click();
a.remove();
};
Both has a function to export the data as an image.
Import and Export in JSON
Fabricjs
const handleJSONImport = () => {
if (!canvas || !textAreaEl.current) return;
const json = JSON.parse(textAreaEl.current.value);
canvas.loadFromJSON(json, () => {
canvas.renderAll();
});
};
const handleJSONExport = () => {
if (!canvas || !textAreaEl.current) return;
const json = JSON.stringify(canvas.toJSON());
textAreaEl.current.value = json;
};
Konvajs
const handleJSONExport = () => {
if (!textAreaEl.current) return;
textAreaEl.current.value = JSON.stringify({
bgImgSrc: bgImg?.src,
bgImgRotation,
shapes,
});
};
const handleJSONImport = () => {
if (!textAreaEl.current) return;
try {
const data: {
bgImgSrc?: string;
bgImgRotation: number;
shapes: PaintShape[];
} = JSON.parse(textAreaEl.current.value);
if (data.bgImgSrc) {
const image = new window.Image();
image.src = data.bgImgSrc;
image.addEventListener("load", () => {
setBgImg(image);
});
}
setBgImgRotation(data.bgImgRotation);
setShapes(data.shapes);
} catch {
alert("Falied to import JSON");
}
};
Fabricjs
: It has load and export functions in canvas.
Konvajs
: I stored data in states, and rendered components, so, I implemented by myself using JSON.stringify
and JSON.parse
.
Prevent Editing a part of objects
Fabricjs
...
const rect = new fabric.Rect({
...
selectable: !readonly,
});
...
Konvajs
...
<Shape
...
onSelect={
shape.readonly ? undefined : handleSelect(shapeIdx, shape.key)
}
/>
...
Fabricjs
: It has a selectable
option in creator of shapes.
Konvajs
: When a shape was readonly
, I didn't attach onClick
event into the components.
With Fabricjs
, they provide most features that I wanted by default. By setting up options, the library handled most of it for me. At first, I was afraid the fact that I should handle instances but in most of the cases, I didn't need to.
With Konvajs
, I'd like the way because it's the way React does. However, I had to manage the data to use Konva components. And even though I tried to avoid unnecessary rendering cycle with useCallback
, I'm a little worried about the performance. But it'll definitely be better if you put efforts more into this.
Both libraries were really easy to use and have good documentation and there are many examples online.
Some options didn't work in Fabricjs
that I found in the documentation, but, I was able to find the solution easily by searching online.
I've made the examples with Fabricjs
at first, it took just a few hours to its done.
Otherwise, I spent almost ten hours with Konvajs
.
I had to think of the structure and how to manage states and render components. Plus, while I was working with this library, there were a lot of other work to do at my company. It made me to work hard with this library.
There's gonna be just a few days to implement the feature. Even there is much time to work though, this research is really worth.
I will decide that with my team tomorrow which one would be better for us.
I hope it'll be helpful for someone.
Happy Coding!
Top comments (7)
Great article! I was confused between Fabric and Konva as well. This helped me come to a decision. Guess I'll give Fabric a try first.
And what did you decide to choose? I think you have chosen React Konvo.
We ended up deciding to use Fabricjs. We only had about one week to the deadline, we needed to develop in time. We found Fabricjs brought us more faster development than React Konva.
Great, Thanks
Helpful article, thanks
Can we handle zoom in fabric js
is it possible to resize the image