React H5中实现给图片进行添加箭头、圆圈等标记
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;
更多推荐


所有评论(0)