<template>
<div>
<canvas
ref="canvas"
:width="canvasWidth"
:height="canvasHeight"
style="border: 1px solid #ccc"
></canvas>
</div>
</template>
<script>
import { fabric } from "fabric";
export default {
name: "ImageAnnotator",
data() {
return {
canvas: null,
canvasWidth: 800,
canvasHeight: 600,
isDrawing: false,
isCtrlPressed: false,
drawingRect: null,
startX: 0,
startY: 0,
hasMoved: false,
imageBounds: null,
boxCount: 0,
labelMap: new Map(),
};
},
mounted() {
this.initCanvas();
this.loadBackgroundImage();
this.bindKeyboardEvents();
},
methods: {
initCanvas() {
this.canvas = new fabric.Canvas(this.$refs.canvas, {
selection: true,
preserveObjectStacking: true,
});
this.canvas.on("mouse:down", (opt) => {
if (this.isCtrlPressed) {
this.canvas.isDragging = true;
this.canvas.selection = false;
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
return;
}
if (opt.target) return;
const pointer = this.canvas.getPointer(opt.e);
if (!this.isPointInsideImage(pointer)) return;
this.isDrawing = true;
this.hasMoved = false;
this.startX = pointer.x;
this.startY = pointer.y;
this.drawingRect = null;
});
this.canvas.on("mouse:move", (opt) => {
const pointer = this.canvas.getPointer(opt.e);
if (this.canvas.isDragging && this.isCtrlPressed) {
const e = opt.e;
const vpt = this.canvas.viewportTransform;
vpt[4] += e.clientX - this.canvas.lastPosX;
vpt[5] += e.clientY - this.canvas.lastPosY;
this.canvas.lastPosX = e.clientX;
this.canvas.lastPosY = e.clientY;
this.canvas.requestRenderAll();
return;
}
if (!this.isDrawing) return;
const dx = pointer.x - this.startX;
const dy = pointer.y - this.startY;
const newLeft = dx >= 0 ? this.startX : pointer.x;
const newTop = dy >= 0 ? this.startY : pointer.y;
const newRight = newLeft + Math.abs(dx);
const newBottom = newTop + Math.abs(dy);
if (!this.isRectInsideImage(newLeft, newTop, newRight, newBottom)) return;
if (!this.hasMoved && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
this.hasMoved = true;
this.drawingRect = new fabric.Rect({
left: newLeft,
top: newTop,
width: Math.abs(dx),
height: Math.abs(dy),
fill: "rgba(0, 255, 0, 0.3)",
stroke: "green",
strokeWidth: 1,
strokeUniform: true,
selectable: true,
hasControls: true,
hasBorders: true,
lockUniScaling: false,
objectCaching: false,
transparentCorners: false,
});
this.canvas.add(this.drawingRect);
}
if (this.hasMoved && this.drawingRect) {
this.drawingRect.set({
width: Math.abs(dx),
height: Math.abs(dy),
left: newLeft,
top: newTop,
});
this.canvas.requestRenderAll();
}
});
this.canvas.on("mouse:up", () => {
if (this.canvas.isDragging) {
this.canvas.setViewportTransform(this.canvas.viewportTransform);
}
this.canvas.isDragging = false;
this.canvas.selection = true;
if (
this.drawingRect &&
(this.drawingRect.width < 10 || this.drawingRect.height < 10)
) {
this.canvas.remove(this.drawingRect);
} else if (this.drawingRect) {
const rect = this.drawingRect;
this.boxCount++;
setTimeout(() => {
const label = new fabric.Textbox(
`框 ${this.boxCount}\nvalue:我是哈哈哈哈.哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈..${this.boxCount}`,
{
fontSize: 14,
fill: "black",
selectable: false,
evented: false,
width: 100,
textAlign: "left",
originX: "left",
originY: "top",
}
);
this.canvas.add(label);
this.labelMap.set(rect, label);
this.updateLabelPosition(rect);
this.canvas.bringToFront(rect);
this.canvas.requestRenderAll();
}, 1000);
}
this.isDrawing = false;
this.drawingRect = null;
this.hasMoved = false;
});
this.canvas.on("mouse:wheel", (opt) => {
const delta = opt.e.deltaY;
let zoom = this.canvas.getZoom();
zoom *= 0.999 ** delta;
zoom = Math.min(Math.max(zoom, 0.5), 3);
const point = this.canvas.getPointer(opt.e);
this.canvas.zoomToPoint(point, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
});
this.canvas.on("object:moving", (opt) => {
if (opt.target && opt.target.type === "rect") {
this.updateLabelPosition(opt.target);
}
});
this.canvas.on("object:scaling", (opt) => {
if (opt.target && opt.target.type === "rect") {
this.updateLabelPosition(opt.target);
}
});
this.canvas.on("object:rotating", (opt) => {
if (opt.target && opt.target.type === "rect") {
this.updateLabelPosition(opt.target);
}
});
this.canvas.on("selection:created", this.handleSelection);
this.canvas.on("selection:updated", this.handleSelection);
this.canvas.on("selection:cleared", this.clearHighlight);
},
updateLabelPosition(rect) {
const label = this.labelMap.get(rect);
if (!label) return;
const offset = 5;
const labelLeft = rect.left;
const labelTop = rect.top - label.height - offset;
label.set({
left: labelLeft,
top: labelTop,
angle: 0,
scaleX: 1,
scaleY: 1,
});
},
loadBackgroundImage() {
const imageUrl = "https://fabricjs.cc/assets/github.svg";
fabric.Image.fromURL(imageUrl, (img) => {
img.set({
originX: "left",
originY: "top",
selectable: false,
evented: false,
});
const scaleX = this.canvasWidth / img.width;
const scaleY = this.canvasHeight / img.height;
img.scaleX = scaleX;
img.scaleY = scaleY;
this.canvas.setBackgroundImage(
img,
this.canvas.requestRenderAll.bind(this.canvas),
{
originX: "left",
originY: "top",
}
);
this.imageBounds = {
left: 0,
top: 0,
right: img.width * scaleX,
bottom: img.height * scaleY,
};
});
},
isPointInsideImage(point) {
if (!this.imageBounds) return false;
const { left, top, right, bottom } = this.imageBounds;
return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
},
isRectInsideImage(left, top, right, bottom) {
if (!this.imageBounds) return false;
const bounds = this.imageBounds;
return (
left >= bounds.left &&
top >= bounds.top &&
right <= bounds.right &&
bottom <= bounds.bottom
);
},
handleSelection(e) {
this.clearHighlight();
const activeObj = e.selected?.[0];
if (activeObj && activeObj.type === "rect") {
activeObj.set({
stroke: "darkred",
fill: "rgba(255, 0, 0, 0.3)",
});
this.canvas.requestRenderAll();
}
},
clearHighlight() {
this.canvas.getObjects("rect").forEach((obj) => {
obj.set({
stroke: "green",
fill: "rgba(0, 255, 0, 0.3)",
});
});
this.canvas.requestRenderAll();
},
bindKeyboardEvents() {
window.addEventListener("keydown", this.handleKeyDown);
window.addEventListener("keyup", this.handleKeyUp);
},
handleKeyDown(e) {
if (e.key === "Control") {
this.isCtrlPressed = true;
}
},
handleKeyUp(e) {
if (e.key === "Control") {
this.isCtrlPressed = false;
this.canvas.isDragging = false;
}
},
},
beforeDestroy() {
window.removeEventListener("keydown", this.handleKeyDown);
window.removeEventListener("keyup", this.handleKeyUp);
this.canvas.dispose();
},
};
</script>
<style scoped>
canvas {
display: block;
margin: 0 auto;
}
</style>