You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:React Three Fiber中Fabric.js作为CanvasTexture的交互及文本编辑实现方案

问题

我已在React Three Fiber应用中成功将Fabric.js以CanvasTexture的形式集成,将Fabric画布映射到平面几何体上,当前画布内容可正常显示但无交互效果。我希望实现Fabric画布上的指针事件,如文本编辑、图像移动、纹理操作等功能,请问是否有可行的解决方案?

现有实现代码

import { useEffect, useRef, useState } from "react";
import { fabric } from "fabric";

export default function Plane() {
  
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const textureRef = useRef() as any;
  const [fabricCanvas, setFabricCanvas] = useState<fabric.Canvas >(null!);

  useEffect(() => {
    const canvasEl = document.createElement("canvas");
    canvasEl.width = 1024;
    canvasEl.height = 1024;
    const fCanvas = new fabric.Canvas(canvasEl);
    setFabricCanvas(fCanvas);
    canvasRef.current = canvasEl;
  }, []);

  useEffect(() => {
    if (fabricCanvas) { 
      fabricCanvas.on("after:render", () => {
        textureRef.current.needsUpdate = true;
      });
    }
  }, [fabricCanvas]);

  function addRect() {
    const textBox = new fabric.Textbox('Hello', {
      width: 250,
      top: 50,
      left: 50,
      textBackgroundColor: 'red',
      fontSize: 22,
      textAlign: 'left',
      padding: 2,
      editable: true
    });
    if (fabricCanvas) { 
      fabricCanvas.add(textBox);
      fabricCanvas.setActiveObject(textBox);
    }
  }

  return (
    <mesh
    onClick={addRect}
    >
      <planeGeometry args={[1, 1]} />
      <meshStandardMaterial attach="material" metalness={0} roughness={1}>
        <canvasTexture
          ref={textureRef}
          attach="map"
          image={canvasRef.current}
        ></canvasTexture>
      </meshStandardMaterial>
    </mesh>
  );
}
解决方案

核心思路是将Three.js中的鼠标/触摸事件坐标转换为Fabric画布的本地坐标,再手动触发Fabric对应的交互事件,完整实现如下:

修改后的完整代码

import { useEffect, useRef, useState } from "react";
import { fabric } from "fabric";
import { useThree, useRaycaster } from "@react-three/fiber";
import * as THREE from "three";

export default function Plane() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const textureRef = useRef<any>(null);
  const [fabricCanvas, setFabricCanvas] = useState<fabric.Canvas | null>(null);
  const { camera, gl } = useThree();
  const raycaster = useRaycaster();
  const planeRef = useRef<any>(null);

  useEffect(() => {
    const canvasEl = document.createElement("canvas");
    canvasEl.width = 1024;
    canvasEl.height = 1024;
    const fCanvas = new fabric.Canvas(canvasEl, {
      enableRetinaScaling: false, // 避免缩放干扰坐标转换
    });
    setFabricCanvas(fCanvas);
    canvasRef.current = canvasEl;

    // 文本编辑时手动聚焦隐藏输入框
    fCanvas.on("text:editing:started", () => {
      const activeText = fCanvas.getActiveObject() as fabric.Textbox;
      if (activeText) {
        const hiddenInput = document.querySelector(
          ".fabric-textbox-editor"
        ) as HTMLInputElement;
        hiddenInput?.focus();
      }
    });

    return () => fCanvas.dispose();
  }, []);

  useEffect(() => {
    if (fabricCanvas) {
      fabricCanvas.on("after:render", () => {
        if (textureRef.current) textureRef.current.needsUpdate = true;
      });
    }
  }, [fabricCanvas]);

  // 坐标转换:Three.js鼠标坐标 → Fabric画布像素坐标
  const getFabricCoords = (clientX: number, clientY: number) => {
    if (!planeRef.current || !fabricCanvas) return null;

    // 计算鼠标在平面几何体上的UV交点
    const mouse = new THREE.Vector2(
      (clientX / window.innerWidth) * 2 - 1,
      -(clientY / window.innerHeight) * 2 + 1
    );
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObject(planeRef.current);

    if (intersects.length === 0) return null;

    // 修正UV轴方向差异(Three.js Y轴从下到上,Fabric从上到下)
    const uv = intersects[0].uv;
    return {
      x: uv.x * fabricCanvas.getWidth(),
      y: (1 - uv.y) * fabricCanvas.getHeight(),
    };
  };

  // 模拟Fabric鼠标按下事件
  const handlePointerDown = (e: React.PointerEvent) => {
    const coords = getFabricCoords(e.clientX, e.clientY);
    if (!coords || !fabricCanvas) return;

    fabricCanvas._onMouseDown({
      clientX: coords.x,
      clientY: coords.y,
      button: e.button,
    } as MouseEvent);
  };

  // 模拟Fabric鼠标移动事件
  const handlePointerMove = (e: React.PointerEvent) => {
    const coords = getFabricCoords(e.clientX, e.clientY);
    if (!coords || !fabricCanvas) return;

    fabricCanvas._onMouseMove({
      clientX: coords.x,
      clientY: coords.y,
    } as MouseEvent);
  };

  // 模拟Fabric鼠标抬起事件
  const handlePointerUp = (e: React.PointerEvent) => {
    const coords = getFabricCoords(e.clientX, e.clientY);
    if (!coords || !fabricCanvas) return;

    fabricCanvas._onMouseUp({
      clientX: coords.x,
      clientY: coords.y,
    } as MouseEvent);
  };

  function addRect() {
    const textBox = new fabric.Textbox('Hello', {
      width: 250,
      top: 50,
      left: 50,
      textBackgroundColor: 'red',
      fontSize: 22,
      textAlign: 'left',
      padding: 2,
      editable: true
    });
    if (fabricCanvas) { 
      fabricCanvas.add(textBox);
      fabricCanvas.setActiveObject(textBox);
    }
  }

  return (
    <mesh
      ref={planeRef}
      onClick={addRect}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onPointerUp={handlePointerUp}
      style={{ touchAction: 'none' }} // 禁用浏览器默认触摸手势
    >
      <planeGeometry args={[1, 1]} />
      <meshStandardMaterial attach="material" metalness={0} roughness={1}>
        <canvasTexture
          ref={textureRef}
          attach="map"
          image={canvasRef.current}
          flipY={false} // 关闭Y轴翻转,匹配Fabric坐标系统
        ></canvasTexture>
      </meshStandardMaterial>
    </mesh>
  );
}

关键注意点

  • 坐标轴修正:Three.js的UV坐标Y轴方向与Fabric画布相反,转换时需用1 - uv.y调整
  • 纹理翻转设置:将canvasTexture的flipY设为false,避免画布内容倒置
  • 文本编辑焦点:Fabric文本框依赖隐藏输入框实现编辑,需在text:editing:started事件中手动聚焦
  • 触摸事件兼容:添加touchAction: 'none'防止浏览器默认触摸行为(如缩放、滚动)干扰交互

内容的提问来源于stack exchange,提问作者Patrick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 21:07:09