在现代前端应用中,图片标注功能被广泛应用于标注系统、图像处理、教育培训等场景。一个交互友好、性能良好的图片标注工具不仅能提高效率,还能提升用户体验。本文将介绍如何使用 Leafer-UI ------ 一款基于 WebCanvas 的高性能图形引擎,来实现一个简洁而实用的图片标注类。
一、Leafer-UI 简介
Leafer-UI 是一个支持图形拖拽、缩放、旋转、文本编辑的可视化 UI 图形框架,底层使用 leafer.ts
提供图形渲染能力,支持像素级精度操作,同时也具备较好的性能优化和响应式特性,适用于图形编辑器、可视化标注、在线设计等应用场景。
二、实现思路
我们要实现的"图片标注类"包含以下核心功能:
- 加载并显示一张图片;
- 支持用户在图片上添加矩形框或文本注释;
- 支持标注框拖动、缩放、删除等交互;
- 导出标注数据(如位置、内容等)以用于后续处理。
三、基础环境搭建
我们以 Vue 3 + Vite 为基础来开发项目,首先安装依赖:
js
npm install leafer-ui
初始化 Leafer:
html
<template>
<div id="leafer-view" ref="leaferViewRef"></div>
</template>
<script setup lang="ts">
import '@leafer-in/editor';
import '@leafer-in/viewport';
import '@leafer-in/view';
import { onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
import { DrawManger } from '@/utils/useDraw';
import { emitter } from '@/utils/mitt';
import { useTagStore } from '@/store';
import { ANNOTATION_STYLE } from '@/utils/parms';
const mode = defineModel('mode', { type: String });
const tagStore = useTagStore();
// 响应式变量
const leaferViewRef = ref<HTMLDivElement>();
let drawManger: DrawManger | null = null;
/**
* 初始化应用
*/
const initApp = async (): Promise<void> => {
if (!leaferViewRef.value) return;
drawManger = new DrawManger(leaferViewRef.value);
try {
await drawManger.init();
} catch (error) {
console.error('初始化失败', error);
}
};
/** 更新画布图片 */
const handleImgUrlChange = async (url: string) => {
if (!drawManger) return;
await drawManger.setImage(url);
};
const handleCommand = async (type: string = 'select'): Promise<void> => {
if (!type && !drawManger) return;
try {
const modeActions: Record<string, () => void> = {
select: () => drawManger?.select(),
edit: () => drawManger?.edit(),
move: () => drawManger?.move(),
zoomIn: () => drawManger?.zoomIn(),
zoomOut: () => drawManger?.zoomOut(),
reset: () => drawManger?.centerImageElement(),
delete: () => drawManger?.clearRectangles()
};
const action = modeActions[type];
if (action) {
action();
}
} catch (e) {
console.error('执行命令出错:', e);
} finally {
await nextTick();
mode.value = '';
}
};
watch(
() => mode.value,
async newVal => {
console.log(newVal);
await handleCommand(newVal);
},
{ immediate: true }
);
watch(
() => tagStore.labelActive,
newVal => {
console.log(newVal, '88888888');
if (newVal !== null) {
console.log(ANNOTATION_STYLE[newVal], '555555555');
drawManger?.setRectStyle(ANNOTATION_STYLE[newVal]);
}
}
);
// 生命周期钩子
onMounted(async () => {
try {
await initApp();
emitter.on('imgUrl', handleImgUrlChange);
} catch (error) {
console.error('应用初始化失败', error);
}
});
onUnmounted(() => {
if (drawManger) {
drawManger.destroy();
}
emitter.off('imgUrl', handleImgUrlChange);
});
</script>
<style scoped>
#leafer-view {
width: 100%;
height: calc(100% - 30px);
}
</style>
四、创建标注类
我们可以封装一个标注类 DrawManger
,用于添加标注元素和管理状态:
ts
import { App, Image, ImageEvent, PointerEvent, Rect, Text } from 'leafer-ui';
import { DotMatrix } from 'leafer-x-dot-matrix';
import * as CONFIG from '@/utils/parms';
import { useTagStore } from '@/store';
import { ElMessage } from 'element-plus';
import { genUUID } from '@/utils/uuid';
const tagStore = useTagStore();
// 操作模式枚举
export enum DrawMode {
SELECT = 'select',
EDIT = 'edit',
MOVE = 'move'
}
// 矩形和标签的组合接口
interface RectWithLabel {
rect: Rect;
label: Text;
id: string; // 唯一标识符
}
export class DrawManger {
public app: App | null = null;
private dotMatrix: DotMatrix | null = null;
private imageElement: Image | null = null;
// 操作模式管理
private currentMode: DrawMode = DrawMode.SELECT;
// 绘制状态管理
private isDrawing: boolean = false;
private startPoint: { x: number; y: number } | null = null;
private currentRect: Rect | null = null;
private currentLabel: Text | null = null;
private rectangleWithLabels: RectWithLabel[] = []; // 存储所有绘制的矩形和标签
// 移动状态管理
private isMovingImage: boolean = false;
private isMovingRect: boolean = false;
private selectedRectWithLabel: RectWithLabel | null = null;
private lastImagePosition: { x: number; y: number } | null = null;
private dragStartPoint: { x: number; y: number } | null = null;
// 绘画矩形UI
private fillColor: string = 'rgb(93,152,214,0.4)';
private strokeColor: string = 'rgb(93,152,214)';
private strokeWidth: number = 4;
// 标签样式配置
private labelStyle = {
fontSize: 12,
fill: '#ffffff',
backgroundColor: 'rgba(0,0,0,0.5)',
padding: [2, 10]
};
constructor(private container: HTMLDivElement) {}
public async init() {
this.app = new App({
view: this.container,
editor: {}
});
this.dotMatrix = new DotMatrix(this.app, {
dotSize: CONFIG.DOT_SIZE,
targetDotMatrixPixel: CONFIG.TARGET_DOT_MATRIX_PIXEL
});
this.dotMatrix.enableDotMatrix(true);
this.bindEventListeners();
}
/**
* 异步设置图像
*/
public async setImage(url: string): Promise<void> {
if (!this.app) throw new Error('app未初始化');
this.clearRectangles();
// 清除之前的图片
if (this.imageElement) {
this.app.tree.remove(this.imageElement);
this.imageElement = null;
}
// 创建一个新的图像实例并设置其属性
return new Promise((resolve, reject) => {
const image = new Image({
url,
draggable: false
});
// 监听图像加载成功的事件
image.once(ImageEvent.LOADED, (e: ImageEvent) => {
try {
// 成功加载后,调用中心化图像的函数,并更新当前图像元素,然后解析Promise
this.centerImage(image, e);
this.imageElement = image;
resolve();
} catch (error) {
// 如果中心化图像过程中出现错误,拒绝Promise
reject(error);
}
});
// 监听图像加载失败的事件
image.once(ImageEvent.ERROR, () => {
// 加载失败时,拒绝Promise并抛出错误
reject(new Error('图片加载失败'));
});
// 如果app未初始化,直接返回
if (!this.app) return;
// 将新图像添加到应用的树结构中
this.app.tree.add(image);
});
}
private centerImage(image: Image, e: ImageEvent) {
if (!this.app) return;
const canvasWidth = this.app.tree.canvas.width / 2;
const canvasHeight = this.app.tree.canvas.height / 2;
const { width, height } = e.image;
image.set({
x: canvasWidth - width / 2,
y: canvasHeight - height / 2
});
}
/** 图片居中 */
public centerImageElement() {
if (!this.app || !this.imageElement) return;
const canvasWidth = this.app.tree.canvas.width / 2;
const canvasHeight = this.app.tree.canvas.height / 2;
const { width, height } = this.imageElement;
if (typeof width !== 'number' || typeof height !== 'number') {
console.warn('图片尺寸无效');
return;
}
this.app.tree.zoom(1);
this.imageElement.set({
x: canvasWidth - width / 2,
y: canvasHeight - height / 2
});
}
/** 设置操作模式 */
public setMode(mode: DrawMode) {
this.currentMode = mode;
this.resetStates();
this.updateRectanglesInteractivity();
this.updateImageInteractivity();
console.log(`切换到${mode}模式`);
}
/** 导出 select 方法 */
public select() {
this.setMode(DrawMode.SELECT);
}
/** 导出 edit 方法 */
public edit() {
this.setMode(DrawMode.EDIT);
}
/** 导出 move 方法 */
public move() {
this.setMode(DrawMode.MOVE);
}
/** 放大 */
public zoomIn() {
if (!this.app) return;
this.app.tree.zoom('in');
}
/** 缩小 */
public zoomOut() {
if (!this.app) return;
this.app.tree.zoom('out');
}
/** 清除所有矩形和标签 */
public clearRectangles() {
if (!this.app) return;
this.rectangleWithLabels.forEach(({ rect, label }) => {
this.app!.tree.remove(rect);
this.app!.tree.remove(label);
});
this.rectangleWithLabels = [];
tagStore.setActive(null);
tagStore.setName('');
tagStore.setHave(false);
// 清除当前正在绘制的矩形和标签
if (this.currentRect) {
this.app.tree.remove(this.currentRect);
this.currentRect = null;
}
if (this.currentLabel) {
this.app.tree.remove(this.currentLabel);
this.currentLabel = null;
}
}
/** 获取所有矩形的坐标信息和标签(相对于图片的坐标) */
public getRectangles(): Array<{
id: string;
label: string;
x: number;
y: number;
width: number;
height: number;
}> {
// 如果没有图片,返回空数组
if (!this.imageElement) {
return [];
}
const imageX = this.imageElement.x as number;
const imageY = this.imageElement.y as number;
return this.rectangleWithLabels.map(({ rect, label, id }) => {
const rectX = rect.x as number;
const rectY = rect.y as number;
const rectWidth = rect.width as number;
const rectHeight = rect.height as number;
return {
id,
label: label.text as string,
// 转换为相对于图片的坐标
x: rectX - imageX,
y: rectY - imageY,
width: rectWidth,
height: rectHeight
};
});
}
/** 从相对于图片的坐标数据恢复标注框 */
public setRectangles(
rectangles: Array<{
id?: string;
label: string;
x: number;
y: number;
width: number;
height: number;
}>
): void {
if (!this.app || !this.imageElement) {
console.warn('应用或图片未初始化');
return;
}
// 清除现有的标注框
this.clearRectangles();
const imageX = this.imageElement.x as number;
const imageY = this.imageElement.y as number;
rectangles.forEach(rectData => {
// 将相对坐标转换为绝对坐标
const absoluteX = rectData.x + imageX;
const absoluteY = rectData.y + imageY;
// 创建矩形
const rect = this.createRectangle(absoluteX, absoluteY, absoluteX + rectData.width, absoluteY + rectData.height);
// 创建标签
const label = this.createLabel(rectData.label, absoluteX, absoluteY);
this.updateLabelPosition(label, rect);
// 添加到画布
this.app!.tree.add(rect);
this.app!.tree.add(label);
// 创建组合对象并添加到数组
const rectWithLabel: RectWithLabel = {
rect,
label,
id: rectData.id || genUUID()
};
this.rectangleWithLabels.push(rectWithLabel);
this.updateSingleRectInteractivity(rectWithLabel);
});
console.log(`恢复了 ${rectangles.length} 个标注框`);
}
/**
* 释放资源并清理应用程序,销毁应用实例
*/
public destroy() {
if (this.app) {
this.unbindEventListeners();
this.app.destroy();
this.app = null;
}
this.imageElement = null;
this.dotMatrix = null;
this.rectangleWithLabels = [];
this.currentRect = null;
this.currentLabel = null;
this.startPoint = null;
this.selectedRectWithLabel = null;
this.dragStartPoint = null;
this.lastImagePosition = null;
this.isDrawing = false;
this.isMovingImage = false;
this.isMovingRect = false;
}
/** 重置所有状态 */
private resetStates() {
this.isDrawing = false;
this.isMovingImage = false;
this.isMovingRect = false;
this.startPoint = null;
this.selectedRectWithLabel = null;
this.dragStartPoint = null;
this.lastImagePosition = null;
this.fillColor = 'rgb(93,152,214,0.2)';
this.strokeColor = 'rgb(93,152,214)';
this.strokeWidth = 5;
// 清除当前绘制的矩形和标签
if (this.currentRect && this.app) {
this.app.tree.remove(this.currentRect);
this.currentRect = null;
}
if (this.currentLabel && this.app) {
this.app.tree.remove(this.currentLabel);
this.currentLabel = null;
}
}
/** 更新矩形的交互性 */
private updateRectanglesInteractivity() {
this.rectangleWithLabels.forEach(({ rect, label }) => {
switch (this.currentMode) {
case DrawMode.SELECT:
rect.set({ draggable: false, editable: false, cursor: 'default' });
label.set({ draggable: false, cursor: 'default' });
break;
case DrawMode.EDIT:
rect.set({ draggable: true, editable: true, cursor: 'move' });
label.set({ draggable: false, cursor: 'move' });
break;
case DrawMode.MOVE:
rect.set({ draggable: true, editable: false, cursor: 'move' });
label.set({ draggable: false, cursor: 'move' });
break;
}
});
}
/** 更新图片的交互性 */
private updateImageInteractivity() {
if (!this.imageElement) return;
switch (this.currentMode) {
case DrawMode.SELECT:
this.imageElement.set({ draggable: false, editable: false });
break;
case DrawMode.EDIT:
this.imageElement.set({ draggable: false, editable: false });
break;
case DrawMode.MOVE:
this.imageElement.set({ draggable: true, editable: false });
break;
}
}
/** 创建标签 */
private createLabel(text: string, x: number, y: number): Text {
return new Text({
text,
x,
y,
fontSize: this.labelStyle.fontSize,
fill: this.labelStyle.fill,
padding: this.labelStyle.padding,
draggable: false,
boxStyle: {
fill: this.labelStyle.backgroundColor,
stroke: 'black'
}
});
}
/** 更新标签位置 */
private updateLabelPosition(label: Text, rect: Rect) {
const rectX = rect.x as number;
const rectY = rect.y as number;
label.set({
x: rectX,
y: rectY - this.labelStyle.fontSize - this.labelStyle.padding[0] * 2 - 6 // 在矩形上方
});
}
/** 绘画矩形 */
private createRectangle(startX: number, startY: number, endX: number, endY: number): Rect {
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const width = Math.abs(endX - startX);
const height = Math.abs(endY - startY);
return new Rect({
x,
y,
width,
height,
fill: this.fillColor,
stroke: this.strokeColor,
strokeWidth: this.strokeWidth,
draggable: false
});
}
/** 更新当前正在绘制的矩形和标签 */
private updateCurrentRect(endX: number, endY: number) {
if (!this.currentRect || !this.startPoint) return;
const x = Math.min(this.startPoint.x, endX);
const y = Math.min(this.startPoint.y, endY);
const width = Math.abs(endX - this.startPoint.x);
const height = Math.abs(endY - this.startPoint.y);
this.currentRect.set({
x,
y,
width,
height
});
// 同时更新标签位置
if (this.currentLabel) {
this.updateLabelPosition(this.currentLabel, this.currentRect);
}
}
/** 绑定事件监听器 */
private bindEventListeners() {
if (!this.app) return;
this.app.on(PointerEvent.DOWN, this.onPointerDown);
this.app.on(PointerEvent.MOVE, this.onPointerMove);
this.app.on(PointerEvent.UP, this.onPointerUp);
this.app.on(PointerEvent.CLICK, this.onPointerClick);
}
/** 解绑事件监听器 */
private unbindEventListeners() {
if (!this.app) return;
this.app.off(PointerEvent.DOWN, this.onPointerDown);
this.app.off(PointerEvent.MOVE, this.onPointerMove);
this.app.off(PointerEvent.UP, this.onPointerUp);
this.app.off(PointerEvent.CLICK, this.onPointerClick);
}
/** 检查点是否在图片内部 */
private isPointInImage(x: number, y: number): boolean {
if (!this.imageElement) return false;
const imgX = this.imageElement.x as number;
const imgY = this.imageElement.y as number;
const imgWidth = this.imageElement.width as number;
const imgHeight = this.imageElement.height as number;
return x >= imgX && x <= imgX + imgWidth && y >= imgY && y <= imgY + imgHeight;
}
/** 将坐标限制在图片边界内 */
private clampToImageBounds(x: number, y: number): { x: number; y: number } {
if (!this.imageElement) return { x, y };
const imgX = this.imageElement.x as number;
const imgY = this.imageElement.y as number;
const imgWidth = this.imageElement.width as number;
const imgHeight = this.imageElement.height as number;
return {
x: Math.max(imgX, Math.min(x, imgX + imgWidth)),
y: Math.max(imgY, Math.min(y, imgY + imgHeight))
};
}
/** 检查是否可以开始绘制 - 验证label是否为空 */
private canStartDrawing(): boolean {
const labelHave = tagStore.labelHave;
const labelName = tagStore.labelName;
return !!(labelName && labelName.length > 0 && labelHave);
}
/** 显示label验证错误信息 */
private showLabelValidationError() {
ElMessage({
type: 'error',
message: '请先设置标注名称才能开始绘制',
plain: true
});
}
/** SELECT 模式下的鼠标按下处理 */
private handleSelectModeDown(e: PointerEvent) {
const point = e.getPagePoint();
// 只能在图片内部开始绘制矩形
if (!this.isPointInImage(point.x, point.y)) return;
// 验证label是否为空
if (!this.canStartDrawing()) {
this.showLabelValidationError();
return; // 阻止绘制
}
this.startPoint = { x: point.x, y: point.y };
this.isDrawing = true;
this.currentRect = this.createRectangle(point.x, point.y, point.x, point.y);
// 创建对应的标签
const label = `${tagStore.labelName}`;
this.currentLabel = this.createLabel(
label,
point.x,
point.y - this.labelStyle.fontSize - this.labelStyle.padding[0] * 2 - 6
);
this.app!.tree.add(this.currentRect);
this.app!.tree.add(this.currentLabel);
}
/** EDIT 模式下的鼠标按下处理 */
private handleEditModeDown(e: PointerEvent) {
const point = e.getPagePoint();
// 查找点击的矩形
const rectWithLabel = this.findRectWithLabelAtPoint(point.x, point.y);
if (rectWithLabel && this.app) {
// 更改选中编辑颜色为和标注同一种颜色
this.app.editor.config.stroke = rectWithLabel.rect.stroke;
this.app.editor.config.fill = rectWithLabel.rect.fill;
this.app.editor.updateEditTool();
this.selectedRectWithLabel = rectWithLabel;
this.isMovingRect = true;
}
}
/** MOVE 模式下的鼠标按下处理 */
private handleMoveModeDown(e: PointerEvent) {
const point = e.getPagePoint();
// 优先检查是否点击了矩形
const rectWithLabel = this.findRectWithLabelAtPoint(point.x, point.y);
if (rectWithLabel) {
this.selectedRectWithLabel = rectWithLabel;
this.isMovingRect = true;
} else if (this.isPointInImage(point.x, point.y)) {
// 点击图片区域,准备移动图片
this.isMovingImage = true;
this.recordImagePosition();
}
}
/** SELECT 模式下的鼠标移动处理 */
private handleSelectModeMove(e: PointerEvent) {
const point = e.getPagePoint();
if (!this.isDrawing || !this.startPoint || !this.currentRect) return;
const clampedPoint = this.clampToImageBounds(point.x, point.y);
this.updateCurrentRect(clampedPoint.x, clampedPoint.y);
}
/** EDIT 模式下的鼠标移动处理 */
private handleEditModeMove(e: PointerEvent) {
const point = e.getPagePoint();
if (!this.isMovingRect || !this.selectedRectWithLabel || !this.dragStartPoint) return;
const deltaX = point.x - this.dragStartPoint.x;
const deltaY = point.y - this.dragStartPoint.y;
const rect = this.selectedRectWithLabel.rect;
const label = this.selectedRectWithLabel.label;
const newX = (rect.x as number) + deltaX;
const newY = (rect.y as number) + deltaY;
// 限制矩形在图片范围内移动
const clampedPosition = this.clampRectToImageBounds(newX, newY, rect.width as number, rect.height as number);
rect.set(clampedPosition);
this.updateLabelPosition(label, rect);
this.dragStartPoint = { x: point.x, y: point.y };
}
/** MOVE 模式下的鼠标移动处理 */
private handleMoveModeMove(e: PointerEvent) {
const point = e.getPagePoint();
if (this.isMovingRect && this.selectedRectWithLabel && this.dragStartPoint) {
// 移动矩形和标签(与EDIT模式相同)
const deltaX = point.x - this.dragStartPoint.x;
const deltaY = point.y - this.dragStartPoint.y;
const rect = this.selectedRectWithLabel.rect;
const label = this.selectedRectWithLabel.label;
const newX = (rect.x as number) + deltaX;
const newY = (rect.y as number) + deltaY;
const clampedPosition = this.clampRectToImageBounds(newX, newY, rect.width as number, rect.height as number);
rect.set(clampedPosition);
this.updateLabelPosition(label, rect);
this.dragStartPoint = { x: point.x, y: point.y };
} else if (this.isMovingImage && this.imageElement && this.dragStartPoint) {
// 移动图片
const deltaX = point.x - this.dragStartPoint.x;
const deltaY = point.y - this.dragStartPoint.y;
const newX = (this.imageElement.x as number) + deltaX;
const newY = (this.imageElement.y as number) + deltaY;
this.imageElement.set({ x: newX, y: newY });
this.syncRectanglesWithImage();
this.dragStartPoint = { x: point.x, y: point.y };
}
}
/** 将矩形位置限制在图片边界内 */
private clampRectToImageBounds(x: number, y: number, width: number, height: number): { x: number; y: number } {
if (!this.imageElement) return { x, y };
const imgX = this.imageElement.x as number;
const imgY = this.imageElement.y as number;
const imgWidth = this.imageElement.width as number;
const imgHeight = this.imageElement.height as number;
return {
x: Math.max(imgX, Math.min(x, imgX + imgWidth - width)),
y: Math.max(imgY, Math.min(y, imgY + imgHeight - height))
};
}
/** SELECT 模式下的鼠标抬起处理 */
private handleSelectModeUp(e: PointerEvent) {
const point = e.getPagePoint();
if (!this.isDrawing || !this.startPoint || !this.currentRect || !this.currentLabel || !this.app) return;
const clampedPoint = this.clampToImageBounds(point.x, point.y);
const width = Math.abs(clampedPoint.x - this.startPoint.x);
const height = Math.abs(clampedPoint.y - this.startPoint.y);
if (width > 5 && height > 5) {
this.updateCurrentRect(clampedPoint.x, clampedPoint.y);
// 创建矩形和标签的组合对象
const rectWithLabel: RectWithLabel = {
rect: this.currentRect,
label: this.currentLabel,
id: genUUID()
};
this.rectangleWithLabels.push(rectWithLabel);
this.updateSingleRectInteractivity(rectWithLabel);
console.log('矩形和标签绘制完成:', {
id: rectWithLabel.id,
label: this.currentLabel.text,
x: this.currentRect.x,
y: this.currentRect.y,
width: this.currentRect.width,
height: this.currentRect.height
});
} else {
this.app.tree.remove(this.currentRect);
this.app.tree.remove(this.currentLabel);
}
this.isDrawing = false;
this.startPoint = null;
this.currentRect = null;
this.currentLabel = null;
}
/** EDIT 模式下的鼠标抬起处理 */
private handleEditModeUp() {
this.isMovingRect = false;
this.selectedRectWithLabel = null;
}
/** MOVE 模式下的鼠标抬起处理 */
private handleMoveModeUp() {
this.isMovingImage = false;
this.isMovingRect = false;
this.selectedRectWithLabel = null;
this.lastImagePosition = null;
}
/** 查找指定坐标下的矩形和标签组合 */
private findRectWithLabelAtPoint(x: number, y: number): RectWithLabel | null {
for (let i = this.rectangleWithLabels.length - 1; i >= 0; i--) {
const rectWithLabel = this.rectangleWithLabels[i];
const rect = rectWithLabel.rect;
const rectX = rect.x as number;
const rectY = rect.y as number;
const rectWidth = rect.width as number;
const rectHeight = rect.height as number;
if (x >= rectX && x <= rectX + rectWidth && y >= rectY && y <= rectY + rectHeight) {
return rectWithLabel;
}
}
return null;
}
/** 记录图片初始位置 */
private recordImagePosition() {
if (this.imageElement) {
this.lastImagePosition = {
x: this.imageElement.x as number,
y: this.imageElement.y as number
};
}
}
/** 图片移动时同步移动矩形和标签 */
private syncRectanglesWithImage() {
if (!this.imageElement || !this.lastImagePosition) return;
const currentImageX = this.imageElement.x as number;
const currentImageY = this.imageElement.y as number;
const deltaX = currentImageX - this.lastImagePosition.x;
const deltaY = currentImageY - this.lastImagePosition.y;
// 同步移动所有矩形和标签
this.rectangleWithLabels.forEach(({ rect, label }) => {
const currentRectX = rect.x as number;
const currentRectY = rect.y as number;
const currentLabelX = label.x as number;
const currentLabelY = label.y as number;
rect.set({
x: currentRectX + deltaX,
y: currentRectY + deltaY
});
label.set({
x: currentLabelX + deltaX,
y: currentLabelY + deltaY
});
});
// 更新记录的图片位置
this.lastImagePosition = { x: currentImageX, y: currentImageY };
}
/** 更新单个矩形和标签的交互性 */
private updateSingleRectInteractivity(rectWithLabel: RectWithLabel) {
const { rect, label } = rectWithLabel;
switch (this.currentMode) {
case DrawMode.SELECT:
rect.set({ draggable: false, cursor: 'default' });
label.set({ draggable: false, cursor: 'default' });
break;
case DrawMode.EDIT:
case DrawMode.MOVE:
rect.set({ draggable: true, cursor: 'move' });
label.set({ draggable: false, cursor: 'move' });
break;
}
}
/** 鼠标按下 - 根据模式执行不同操作 */
onPointerDown = (e: PointerEvent): void => {
const point = e.getPagePoint();
if (!this.app || !this.imageElement) return;
this.dragStartPoint = { x: point.x, y: point.y };
switch (this.currentMode) {
case DrawMode.SELECT:
this.handleSelectModeDown(e);
break;
case DrawMode.EDIT:
this.handleEditModeDown(e);
break;
case DrawMode.MOVE:
this.handleMoveModeDown(e);
break;
}
};
/** 鼠标移动 - 根据模式执行不同操作 */
onPointerMove = (e: PointerEvent): void => {
if (!this.dragStartPoint) return;
switch (this.currentMode) {
case DrawMode.SELECT:
this.handleSelectModeMove(e);
break;
case DrawMode.EDIT:
this.handleEditModeMove(e);
break;
case DrawMode.MOVE:
this.handleMoveModeMove(e);
break;
}
};
/** 鼠标抬起 - 根据模式执行不同操作 */
onPointerUp = (e: PointerEvent): void => {
switch (this.currentMode) {
case DrawMode.SELECT:
this.handleSelectModeUp(e);
break;
case DrawMode.EDIT:
this.handleEditModeUp();
break;
case DrawMode.MOVE:
this.handleMoveModeUp();
break;
}
// 重置拖拽状态
this.dragStartPoint = null;
};
/** 鼠标点击 */
onPointerClick = (): void => {
// 点击事件可以用于其他功能,比如选择已有的矩形
};
/** 设置几何体样式 */
public setRectStyle(style: any) {
this.fillColor = style.fill;
this.strokeColor = style.stroke;
this.strokeWidth = style.strokeWidth;
}
/** 设置标签样式 */
public setLabelStyle(style: Partial<typeof this.labelStyle>) {
this.labelStyle = { ...this.labelStyle, ...style };
}
/** 更新指定矩形的标签文本 */
public updateLabelText(rectId: string, newText: string): boolean {
const rectWithLabel = this.rectangleWithLabels.find(item => item.id === rectId);
if (rectWithLabel) {
rectWithLabel.label.set({ text: newText });
return true;
}
return false;
}
}
效果
