这个工具是干啥的?
就像用画图软件在图片上画框框:
- 打开一张图片
- 选择红色/黄色/蓝色框框
- 在图片上拖拽画框
- 画错了可以撤销(后退)
- 撤销了又想恢复(前进)
核心代码(去掉复杂术语)
1. 先定义"框框"长什么样
typescript
// 一个框框需要哪些信息?
interface MarkerType {
x: number, // 框框左上角的横坐标
y: number, // 框框左上角的纵坐标
width: number, // 框框有多宽
height: number, // 框框有多高
type: "1" | "2" | "3" // 框框类型:1=红,2=黄,3=蓝
}
// 整张图片的信息
interface ImgMarkerType {
img: string, // 图片网址
markList: MarkerType[] // 所有的框框
}
2. 准备工具和材料
typescript
// 主要的数据
const imgMarkerData = reactive<ImgMarkerType>({
img: "https://图片网址.com", // 要标注的图片
markList: [] // 开始时空的,画了框框就往里加
});
// 当前选中的颜色
const nowType = ref<"1" | "2" | "3">("1"); // 默认红色
// 记录当前显示到第几个框框(用于前进后退)
const nowIndex = ref(0);
// 是否正在画框(鼠标按下才开始画)
const canDraw = ref(false);
3. 最重要的功能:在画布上画画
typescript
function renderCanvas() {
if (!ctx.value) return;
// 1. 先擦干净画布
ctx.value.clearRect(0, 0, 800, 800);
// 2. 画上背景图片
if (img.value) {
ctx.value.drawImage(img.value, 0, 0, 800, 800);
}
// 3. 画所有的框框
imgMarkerData.markList.forEach((marker, index) => {
// 前进后退功能:只画到当前步骤的框框
if (index > nowIndex.value) return;
// 根据类型设置颜色
let color = "red";
if (marker.type === "2") color = "yellow";
if (marker.type === "3") color = "blue";
// 画框框
ctx.value!.strokeStyle = color;
ctx.value!.strokeRect(marker.x, marker.y, marker.width, marker.height);
});
}
4. 鼠标操作:怎么画框框?
typescript
// 鼠标按下:开始画框
function mousedown(e: MouseEvent) {
canDraw.value = true; // 告诉程序:开始画了!
// 计算鼠标点击的位置
const rect = (e.target as HTMLCanvasElement).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 创建一个新框框(开始很小,宽高都是0)
const newMarker: MarkerType = {
x: x,
y: y,
width: 0,
height: 0,
type: nowType.value // 用当前选中的颜色
};
// 把新框框加到列表里
imgMarkerData.markList.push(newMarker);
nowIndex.value = imgMarkerData.markList.length - 1;
}
// 鼠标移动:拖拽调整框框大小
function mousemove(e: MouseEvent) {
if (!canDraw.value) return; // 如果没按下鼠标,就不画
// 计算当前鼠标位置
const rect = (e.target as HTMLCanvasElement).getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
// 找到当前正在画的框框
const currentMarker = imgMarkerData.markList[nowIndex.value];
// 更新框框的大小(当前位置 - 起点位置)
currentMarker.width = currentX - currentMarker.x;
currentMarker.height = currentY - currentMarker.y;
// 重新画一遍(这样就能看到框框在变大)
renderCanvas();
}
// 鼠标松开:结束画框
function mouseup() {
canDraw.value = false; // 告诉程序:画完了!
renderCanvas(); // 最后画一次
}
5. 前进后退功能
typescript
// 后退:撤销上一步
function goBack() {
if (nowIndex.value > 0) {
nowIndex.value--; // 少显示一个框框
renderCanvas();
}
}
// 前进:恢复撤销的操作
function goForward() {
if (nowIndex.value < imgMarkerData.markList.length - 1) {
nowIndex.value++; // 多显示一个框框
renderCanvas();
}
}
6. 切换框框颜色
typescript
// 很简单,就是改一下当前选中的类型
function changeType(type: "1" | "2" | "3") {
nowType.value = type;
}
下面👇🏻说一下主要思想吧:跟着来是实现,实战一下前端的一些功能,以不变应万变。
- 页面加载时:显示图片,准备画布
- 选择颜色:点击"标注1"(红)、"标注2"(黄)、"标注3"(蓝)
- 开始画框 :
- 在图片上按下鼠标(记录起点)
- 拖拽鼠标(框框跟着变大)
- 松开鼠标(框框完成)
- 修改错误 :
- 点"后退":撤销最后一个框框
- 点"前进":恢复刚才撤销的框框
为什么这样设计?
核心思想:数据驱动
- 不直接操作画面,而是操作数据
- 数据变了,画面自动更新
- 就像玩橡皮泥:你只管捏形状,眼睛会自动看到变化
好处:
- 代码清晰好维护
- 功能容易扩展(比如加个绿色框框很简单)
- 前进后退实现简单(只是改个数字)
这样设计后,无论功能多复杂,只要想清楚"数据怎么变",代码就很好写了!