求助: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
相关产品推荐
相关产品推荐

