React: Comparison of JS Canvas Libraries (Konvajs vs Fabricjs)
Tags: react, webdev, tutorial, beginners
Published: 2022-11-13 16:23:39



> 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](https://github.com/fabricjs/fabric.js) - is a powerful and simple Javascrript HTML5 canvas library
- [Konvajs](https://github.com/konvajs/konva) - 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](http://fabricjs.com/), 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**

![Downloads in past one year](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cst6gleaychwowkm33u.png)

`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

![Fabric Contribution Period](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/04y64hn0v6r7v6efbz08.png)

- Konvajs

![Konva Contribution Period](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/drxjlnlm8gz66utwgs9l.png)

- react-konvajs

![React Konva Contribution Period](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqttbvk49m632jem516i.png)



It's been so long since they came out, I think both libraries are mature enough. It's no doubt.

---

## Easy to use

![Default User Interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2y83tnkorrxrkwkfu00.png)

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`
```typescript
...
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`
```typescript
...
<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`
```typescript
...
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`
```typescript
...
<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`
```typescript
...
    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`
```typescript
...
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`
```typescript
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`
```typescript
...
  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`
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
...
const rect = new fabric.Rect({
...
    selectable: !readonly,
  });
...
```

`Konvajs`
```typescript
...
<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!

---

[Fabricjs Example Github](https://github.com/hsk-kr/react-fabricjs-example)
[Fabricjs Example Preview](https://hsk-kr.github.io/react-fabricjs-example/)

[React Konvajs Example Github](https://github.com/hsk-kr/react-konvajs-example)
[React Konvajs Example Preview](https://hsk-kr.github.io/react-konvajs-example/)