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

import { fabric } from "fabric";

const ImageMarkEditor: React.FC = () => {

  const canvasRef = useRef<HTMLCanvasElement>(null);

  const containerRef = useRef<HTMLDivElement>(null);

  const textInputRef = useRef<HTMLInputElement>(null);

  const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);

  const [canvasSize, setCanvasSize] = useState({ width: 800, height: 600 });

  const [isAddingText, setIsAddingText] = useState(false);

  const [textContent, setTextContent] = useState("");

  const [selectedTextObject, setSelectedTextObject] = useState<fabric.Text | null>(null);

  // 响应式画布尺寸处理

  useEffect(() => {

    const updateCanvasSize = () => {

      if (containerRef.current) {

        const containerWidth = containerRef.current.clientWidth;

        const width = Math.min(containerWidth, 800);

        const height = (width * 600) / 800;

        setCanvasSize({ width, height });

      }

    };

    updateCanvasSize();

    window.addEventListener("resize", updateCanvasSize);

    return () => {

      window.removeEventListener("resize", updateCanvasSize);

    };

  }, []);

  // 初始化 fabric.js canvas

  useEffect(() => {

    if (canvasRef.current) {

      const newCanvas = new fabric.Canvas(canvasRef.current, {

        width: canvasSize.width,

        height: canvasSize.height,

        backgroundColor: "#f0f0f0"

      });

      setCanvas(newCanvas);

      // 监听对象选择事件

      newCanvas.on("selection:created", handleObjectSelection);

      newCanvas.on("selection:updated", handleObjectSelection);

      newCanvas.on("selection:cleared", handleObjectDeselection);

      return () => {

        newCanvas.off("selection:created", handleObjectSelection);

        newCanvas.off("selection:updated", handleObjectSelection);

        newCanvas.off("selection:cleared", handleObjectDeselection);

        newCanvas.dispose();

      };

    }

  }, [canvasSize]);

  // 处理对象选择

  const handleObjectSelection = (e: fabric.IEvent) => {

    const selected = e.selected?.[0];

    if (selected && selected instanceof fabric.Text) {

      setSelectedTextObject(selected);

      setTextContent(selected.text || "");

      setIsAddingText(true);

    } else {

      setSelectedTextObject(null);

      setIsAddingText(false);

    }

  };

  // 处理对象取消选择

  const handleObjectDeselection = () => {

    setSelectedTextObject(null);

    setIsAddingText(false);

    setTextContent("");

  };

  // 处理图片上传并自适应画布

  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {

    const file = e.target.files?.[0];

    if (file && canvas) {

      const reader = new FileReader();

      reader.onloadend = () => {

        const img = new Image();

        img.onload = () => {

          const scaleX = canvasSize.width / img.width;

          const scaleY = canvasSize.height / img.height;

          const scale = Math.min(scaleX, scaleY, 1);

          const fabricImage = new fabric.Image(img, {

            scaleX: scale,

            scaleY: scale,

            left: (canvasSize.width - img.width * scale) / 2,

            top: (canvasSize.height - img.height * scale) / 2,

            selectable: false

          });

          canvas.clear();

          canvas.add(fabricImage);

          canvas.renderAll();

          handleObjectDeselection();

        };

        img.src = reader.result as string;

      };

      reader.readAsDataURL(file);

    }

  };

  // 添加矩形框

  const addRectangle = () => {

    if (canvas) {

      const rect = new fabric.Rect({

        left: canvasSize.width * 0.2,

        top: canvasSize.height * 0.2,

        fill: "transparent",

        stroke: "red",

        width: canvasSize.width * 0.2,

        height: canvasSize.height * 0.15,

        strokeWidth: 2

      });

      canvas.add(rect);

      canvas.discardActiveObject().renderAll();

    }

  };

  // 添加圆形框

  const addCircle = () => {

    if (canvas) {

      const circle = new fabric.Circle({

        left: canvasSize.width * 0.4,

        top: canvasSize.height * 0.2,

        radius: Math.min(canvasSize.width, canvasSize.height) * 0.08,

        fill: "transparent",

        stroke: "red",

        strokeWidth: 2

      });

      canvas.add(circle);

      canvas.discardActiveObject().renderAll();

    }

  };

  // 添加箭头 - 修复箭头方向问题

  const addArrow = () => {

    if (canvas) {

      const startX = canvasSize.width * 0.1;

      const startY = canvasSize.height * 0.1;

      const endX = canvasSize.width * 0.3;

      const endY = canvasSize.height * 0.1;

      // 创建线段

      const line = new fabric.Line([startX, startY, endX, endY], {

        stroke: "red",

        strokeWidth: 3,

        selectable: true

      });

      // 创建箭头头部

      const arrowHead = new fabric.Triangle({

        width: 15,

        height: 15,

        fill: "red",

        originX: "center",

        originY: "center",

        left: endX,

        top: endY,

        selectable: false

      });

      // 更新箭头方向的函数

      const updateArrowDirection = () => {

        // 更新箭头位置和角度

        arrowHead.set({

          left: line.x2,

          top: line.y2,

          angle: 90

        });

        canvas?.renderAll();

      };

      // 绑定事件监听器

      line.on("moving", updateArrowDirection);

      line.on("scaling", updateArrowDirection);

      line.on("rotating", updateArrowDirection);

      line.on("modified", updateArrowDirection);

      // 初始设置箭头方向

      updateArrowDirection();

      // 创建组

      const arrowGroup = new fabric.Group([line, arrowHead], {

        selectable: true

      });

      canvas.add(arrowGroup);

      canvas.discardActiveObject().renderAll();

    }

  };

  // 开始添加文字

  const startAddingText = () => {

    setIsAddingText(true);

    setSelectedTextObject(null);

    setTextContent("");

    setTimeout(() => {

      textInputRef.current?.focus();

    }, 100);

  };

  // 确认添加/编辑文字

  const confirmText = () => {

    if (!canvas || !textContent.trim()) return;

    if (selectedTextObject) {

      // 编辑现有文字

      selectedTextObject.set({ text: textContent });

      canvas.renderAll();

    } else {

      // 添加新文字

      const text = new fabric.Text(textContent, {

        left: canvasSize.width / 2,

        top: canvasSize.height / 2,

        fontFamily: "Arial",

        fontSize: 20,

        fill: "#ff0000",

        backgroundColor: "rgba(255, 255, 255, 0.7)",

        padding: 5,

        editable: true

      });

      canvas.add(text);

      canvas.setActiveObject(text);

      canvas.renderAll();

    }

    // 重置状态

    setIsAddingText(false);

    setTextContent("");

  };

  // 取消文字编辑

  const cancelText = () => {

    setIsAddingText(false);

    setTextContent("");

    setSelectedTextObject(null);

    if (canvas) {

      canvas.discardActiveObject().renderAll();

    }

  };

  // 删除选中的文字

  const deleteSelectedText = () => {

    if (canvas && selectedTextObject) {

      canvas.remove(selectedTextObject);

      canvas.renderAll();

      cancelText();

    }

  };

  // 清空所有标记

  const clearMarks = () => {

    if (canvas) {

      const objects = canvas.getObjects();

      objects.forEach(obj => {

        if (!(obj instanceof fabric.Image)) {

          canvas.remove(obj);

        }

      });

      canvas.renderAll();

      handleObjectDeselection();

    }

  };

  // 保存编辑后的图片

  const saveImage = () => {

    if (canvas) {

      const dataUrl = canvas.toDataURL({

        format: "png",

        quality: 1

      });

      const link = document.createElement("a");

      link.href = dataUrl;

      link.download = "edited-image.png";

      link.click();

    }

  };

  return (

    <div className="flex flex-col items-center p-4 w-full">

      {/* 上传图片 */}

      <input type="file" accept="image/*" onChange={handleImageUpload} className="mb-4 w-full max-w-md" />

      {/* 文字编辑输入框 */}

      {isAddingText && (

        <div className="mb-4 p-4 bg-white border rounded-lg shadow-lg w-full max-w-md">

          <div className="flex flex-col gap-3">

            <input

              ref={textInputRef}

              type="text"

              value={textContent}

              onChange={e => setTextContent(e.target.value)}

              placeholder="请输入文字内容"

              className="border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"

              onKeyDown={e => {

                if (e.key === "Enter") confirmText();

                if (e.key === "Escape") cancelText();

              }}

            />

            <div className="flex gap-2">

              <button

                onClick={confirmText}

                disabled={!textContent.trim()}

                className="flex-1 bg-blue-500 text-white py-2 px-4 rounded disabled:bg-gray-300 disabled:cursor-not-allowed"

              >

                {selectedTextObject ? "更新文字" : "添加文字"}

              </button>

              <button onClick={cancelText} className="flex-1 bg-gray-500 text-white py-2 px-4 rounded">

                取消

              </button>

              {selectedTextObject && (

                <button onClick={deleteSelectedText} className="flex-1 bg-red-500 text-white py-2 px-4 rounded">

                  删除

                </button>

              )}

            </div>

          </div>

        </div>

      )}

      {/* Canvas 显示区域 */}

      <div ref={containerRef} className="w-full max-w-[800px] mb-4 border rounded-lg overflow-hidden">

        <canvas ref={canvasRef} width={canvasSize.width} height={canvasSize.height} />

      </div>

      {/* 控制按钮 */}

      <div className="flex flex-wrap gap-2 justify-center w-full max-w-[800px]">

        <button onClick={addRectangle} className="bg-blue-500 text-white py-2 px-4 rounded text-sm md:text-base flex-1 min-w-[120px]">

          添加矩形框

        </button>

        <button onClick={addCircle} className="bg-red-500 text-white py-2 px-4 rounded text-sm md:text-base flex-1 min-w-[120px]">

          添加圆形框

        </button>

        <button onClick={addArrow} className="bg-green-500 text-white py-2 px-4 rounded text-sm md:text-base flex-1 min-w-[120px]">

          添加箭头

        </button>

        <button onClick={startAddingText} className="bg-purple-500 text-white py-2 px-4 rounded text-sm md:text-base flex-1 min-w-[120px]">

          添加文字

        </button>

        <button onClick={clearMarks} className="bg-yellow-500 text-white py-2 px-4 rounded text-sm md:text-base flex-1 min-w-[120px]">

          清空标记

        </button>

        <button onClick={saveImage} className="bg-gray-500 text-white py-2 px-4 rounded text-sm md:text-base flex-1 min-w-[120px]">

          保存图片

        </button>

      </div>

    </div>

  );

};

export default ImageMarkEditor;

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐