DEV Community 👩‍💻👨‍💻

Cover image for React: Comparison of JS Canvas Libraries (Konvajs vs Fabricjs)
SeongKuk Han
SeongKuk Han

Posted on

React: Comparison of JS Canvas Libraries (Konvajs vs Fabricjs)

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

Downloads in past one 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

Fabric Contribution Period

  • Konvajs

Konva Contribution Period

  • react-konvajs

React Konva Contribution Period

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

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);
...
Enter fullscreen mode Exit fullscreen mode

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}
/>
...
Enter fullscreen mode Exit fullscreen mode

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);
...
Enter fullscreen mode Exit fullscreen mode

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]);
...
Enter fullscreen mode Exit fullscreen mode

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),
  });
}
...
Enter fullscreen mode Exit fullscreen mode

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}
        />
      )}
    </>
  );
};
...
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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");
  }
};
Enter fullscreen mode Exit fullscreen mode

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,
  });
...
Enter fullscreen mode Exit fullscreen mode

Konvajs

...
<Shape
...
    onSelect={
      shape.readonly ? undefined : handleSelect(shapeIdx, shape.key)
    }
  />
...
Enter fullscreen mode Exit fullscreen mode

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
Fabricjs Example Preview

React Konvajs Example Github
React Konvajs Example Preview

Top comments (0)

16 Libraries You Should Know as a React Developer

>> Check out this classic DEV post <<