Search code examples
reactjstypescriptfabricjsnext.js13

When I load Fabric.js canvas elements from JSON, they are not visible on the canvas


Within my app I'd like to maintain the state of a canvas between fullscreen and in-page mode. Right now I'm trying to accomplish this by saving the canvas objects in JSON format within the canvas context. When the mode is switched, a new canvas instance is created, and I load the objects from JSON into that new canvas instance.

The problem is that the objects don't show up. When I log the canvas instance's objects after performing this JSON load, I can see that they are present within the canvas. However, they simply aren't visible on the canvas when I look at the page.

Am I loading the JSON wrong? The new canvas will likely have a different size than the old canvas, but it's not that the objects are stretched or distorted, they simply aren't present. Even when I toggle back to the old canvas size, nothing is visible.

The documentation mentions nothing about this, and I have found no one else with this issue.

The following is my code, limited to what is relevant for the issue.

I do render the canvas immediately after loading the JSON elements.

import { useCanvas } from "@/contexts/CanvasContext";
import React, { useEffect, useRef, useState } from "react";

declare global {
  interface Window {
    fabric: any;
  }
}

type FabricCanvasProps = {
  dimensions: { width: number; height: number };
  canvasInstance: any;
  setCanvasInstance: (canvasInstance: any) => void;
};

const FabricCanvas: React.FC<FabricCanvasProps> = ({
  dimensions,
  canvasInstance,
  setCanvasInstance,
}) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const [prevDimensions, setPrevDimensions] = useState({ width: 0, height: 0 });

  const {
    activeTool,
    setIsDrawnOn,

    canvasData,
    setCanvasData,
  } = useCanvas();

  useEffect(() => {
    const fabricScriptLoaded = () => !!window.fabric;

    if (canvasRef.current && !canvasInstance && fabricScriptLoaded()) {
      const instance = new window.fabric.Canvas(canvasRef.current, {
        isDrawingMode: activeTool !== null,
        height: dimensions.height,
        width: dimensions.width,
        selection: false,
      });

      if (canvasData) {
        instance.loadFromJSON(canvasData, instance.renderAll.bind(instance));
        console.log(canvasData);
        console.log(instance.getObjects());
      }

      // Listen for changes to the canvas
      const updateDrawnState = () => {
        setIsDrawnOn(true);
      };

      instance.on("object:added", updateDrawnState);
      instance.on("object:modified", updateDrawnState);

      setCanvasInstance(instance);
    }
  }, []); // Initializes the canvas instance only once

  // Updates the context with most recent canvas data for re-rendering
  useEffect(() => {
    if (canvasInstance) {
      // Listen for any change that should update the canvas data
      const onChange = () => {
        setCanvasData(canvasInstance.toJSON());
      };

      canvasInstance.on("object:added", onChange);
      canvasInstance.on("object:modified", onChange);
      canvasInstance.on("object:removed", onChange);

      return () => {
        canvasInstance.off("object:added", onChange);
        canvasInstance.off("object:modified", onChange);
        canvasInstance.off("object:removed", onChange);
      };
    }
  }, [canvasInstance]);

// ... Some more useEffects that have to do with canvas size


  return (
    <>
      <canvas ref={canvasRef} className="w-full h-full" />
    </>
  );
};

export default FabricCanvas;


Solution

  • I eventually figured this out: it actually did have to do with canvas updates I thought were unrelated. I was using the following code to resize objects within the canvas as the page size changes:

      const scaleX = dimensions.width / prevDimensions.width;
      const scaleY = dimensions.height / prevDimensions.height;
    
      canvasInstance.getObjects().forEach((obj: any) => {
        obj.scaleX *= scaleX;
        obj.scaleY *= scaleY;
        obj.left *= scaleX;
        obj.top *= scaleY;
        obj.setCoords();
      }
    

    But when the component initially rendered, my prevDimensions were 0. This caused the objects to scale to an infinite size, while throwing no errors or indication that something was wrong, so I couldn't see them.

    I fixed this by adding a check to make sure division by 0 wasn't happening here.