Fabric in Nextjs 实现图片编辑器

目标:在nextjs中实现一个图片编辑器

首先需要适配下nextjs,写一个BaseCavans组件:

import * as fabric from "fabric";
import React, { useEffect, useRef } from "react";

const DEV_MODE = process.env.NODE_ENV === "development";

declare global {
  var canvas: fabric.Canvas | undefined;
}

const BaseCanvas = React.forwardRef<
  fabric.Canvas,
  { onLoad?(canvas: fabric.Canvas): void }
>(({ onLoad }, ref) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (!canvasRef.current) {
      return;
    }

    const canvas = new fabric.Canvas(canvasRef.current);

    // set canvas corner style
    Object.assign(fabric.FabricObject.ownDefaults, {
      cornerColor: "#fff",
      cornerStyle: "circle",
      borderColor: "#8142f6",
      borderScaleFactor: 2, //border thickness
      transparentCorners: false,
      cornerStrokeColor: "#8142f6",
      cornerSize: 8,
      padding: 6,
    });

    DEV_MODE && (window.canvas = canvas);

    if (typeof ref === "function") {
      ref(canvas);
    } else if (typeof ref === "object" && ref) {
      ref.current = canvas;
    }

    // it is crucial `onLoad` is a dependency of this effect
    // to ensure the canvas is disposed and re-created if it changes
    onLoad?.(canvas);

    return () => {
      DEV_MODE && delete window.canvas;

      if (typeof ref === "function") {
        ref(null);
      } else if (typeof ref === "object" && ref) {
        ref.current = null;
      }

      // `dispose` is async
      // however it runs a sync DOM cleanup
      // its async part ensures rendering has completed
      // and should not affect react
      canvas.dispose();
    };
  }, [canvasRef, onLoad]);

  return <canvas ref={canvasRef} />;
});

export default BaseCanvas;

BaseCanvas.displayName = "BaseCanvas";

然后在引入初始化cavans:


const onLoad = useCallback(
      (canvas: fabric.Canvas) => {
        canvas.setDimensions({
          width: 378,
          height: 504,
        });

        // 可添加一些初始化的元素,比如img 、text等

          // 添加背景图
           // Set the clipPath on the image instance
          //imgInstance.clipPath = clipPath;
          //canvas.add(imgInstance);
          //canvas.sendObjectToBack(imgInstance);



        setCanvas(canvas);
      },
      [ref]
    );



<BaseCanvas ref={ref} onLoad={onLoad} />

比较常见的Fabric api说明:

const textbox = new fabric.Textbox('Hello World', {
  left: 100,
  top: 100,
  width: 200,
  fontSize: 20,
  fontFamily: 'Arial',
  fill: '#000000'
});
canvas.add(textbox);

// 2. 创建图片
fabric.Image.fromURL('image.jpg', (img) => {
  img.set({
    left: 100,
    top: 100,
    scaleX: 0.5,
    scaleY: 0.5
  });
  canvas.add(img);
});

// 3. 创建形状
const rect = new fabric.Rect({
  left: 100,
  top: 100,
  width: 100,
  height: 100,
  fill: 'red'
});
canvas.add(rect);

// 4. 添加阴影效果
object.set('shadow', new fabric.Shadow({
  color: 'rgba(0,0,0,0.3)',
  blur: 10,
  offsetX: 5,
  offsetY: 5
}));

// 5. 批量修改选中对象
const activeObjects = canvas.getActiveObjects();
activeObjects.forEach(obj => {
  obj.set('fill', 'red');
});
canvas.requestRenderAll();

遇到的一些问题:

  • Fabric官方文档以html版本说明并不适配nextjs
  • Fabric版本更新,文档还未更新
  • 在特殊情况下(比如Modal )文本输入框会失去焦点,输入不了

解决方法:

  1. 可以根据AI工具实现功能,但是也可能生成api错误,这个时候就需要看看源码fabric的属性了
  2. 如果还是找不到,还可以直接去搜搜github.com/fabricjs/fa… 生成input框引发的样式问题(定位不准等)导致,只需要加上textbox.hiddenTextareaContainer = canvas.lowerCanvasEl.parentNode;告诉 Fabric.js 将隐藏的 textarea 放在画布的父元素中

Appendix            

github.com/fabricjs/fa…

github.com/fabricjs/fa…

github.com/fabricjs/fa…