前言
本文将开始详细介绍图像标注工具的开发过程。主要包括布局、事件类型、图形类设计等内容。
布局
整个工具的布局是比较简洁的。分为上中下三个部分:
- 顶部工具栏:主要包括放大、缩小、旋转、平移、全屏、清除、配置等各种操作。
- 中间绘制区:为主要的图片展示区域和数据图形绘制区域。
- 底部自定义:本质是一个插槽,可以方便用户添加自己的逻辑。如:加载图片、标注数据处理等。 显然,中间的绘制区是最为重要的部分,它内部又划分为两个部分:
- 左侧图形选择区:可以选择不同的图形,从而再绘制区进行绘制。
- 图形展示和绘制区:用来展示图形,并在图像上进行图形绘制。
最终形成的布局如下图所示:
这里有一点需要特别注意: 如果我们把图片显示在canvas上,然后直接在这个canvas上绘制图形,那么当需要撤销编辑操作时,会对原图像展示影响。因此,在工具设计时,设置了两个canvas,一个用来呈现图像,另一个用来绘制图形,两者通过与父元素的绝对定位来实现重合布局。
图形类
图标标注工具的核心在于不同图形的绘制。我采用的是设计一个父类---图形类,然后其他具体的图形(如矩形、多边形等)都是继承于该类。图形类的设计如下:
const config = {
PATH_LINEWIDTH:1,
PATH_STROKESTYLE:"#f00",
POINT_LINEWIDTH:2,
POINT_STROKESTYLE:"#999",
POINT_RADIS:5
}
class Graph {
// 构造函数:主要包含图形的基本信息
constructor(point,options={}){
this.x = Math.round(point.x)
this.y = Math.round(point.y)
this.points = []
this.points.push(point)
this.options = options
this.path_lineWidth = options.path_lineWidth || config.PATH_LINEWIDTH
this.path_strokeStyle = options.path_strokeStyle || config.PATH_STROKESTYLE
this.point_radis = options.point_radis|| config.POINT_RADIS
this.point_lineWidth = options.point_lineWidth || config.POINT_LINEWIDTH
this.point_strokeStyle = options.point_strokeStyle || config.POINT_STROKESTYLE
}
// 计算图形的中心点:在进行图形平移时会使用到
computedCenter() {
let x_sum = 0,y_sum = 0;
this.points.forEach(p => {
x_sum += p.x;
y_sum += p.y;
});
this.x = Math.round(x_sum / this.points.length);
this.y = Math.round(y_sum / this.points.length);
}
// 移动方法
move(startPoint,endPoint) {
let x1 = endPoint.x - startPoint.x;
let y1 = endPoint.y - startPoint.y;
this.points = this.points.map(item => {
return {
x: item.x + x1,
y: item.y + y1,
}
})
this.computedCenter()
}
// 点更新方法
update(i,point){
this.points[i] = point
this.computedCenter()
}
// 根据图形的基本要素创建图形
createPath(ctx) {
ctx.beginPath();
ctx.lineWidth = this.path_lineWidth;
ctx.strokeStyle = this.path_strokeStyle;
this.points.forEach((p, i) => {
ctx[i == 0 ? "moveTo" : "lineTo"](p.x, p.y);
});
ctx.closePath();
}
// 是否在路径中:用于更新和平移图形
isInPath(ctx, point) {
// in the point
for (let i = 0; i < this.points.length; i++) {
ctx.beginPath();
ctx.arc(this.points[i].x, this.points[i].y, this.point_radis, 0, Math.PI * 2, false);
if (ctx.isPointInPath(point.x, point.y)) {
return i;
}
}
// in the figure
this.createPath(ctx);
if (ctx.isPointInPath(point.x, point.y)) {
return 999;
}
return -1;
}
// 绘制控制点,当图形被选中时显示
drawPoints(ctx) {
ctx.save();
ctx.lineWidth = this.point_lineWidth;
ctx.strokeStyle = this.point_strokeStyle;
ctx.fillStyle = this.point_strokeStyle;
this.points.forEach(p => {
ctx.beginPath();
ctx.moveTo(p.x - this.point_radis, p.y - this.point_radis);
ctx.lineTo(p.x - this.point_radis, p.y + this.point_radis);
ctx.lineTo(p.x + this.point_radis, p.y + this.point_radis);
ctx.lineTo(p.x + this.point_radis, p.y - this.point_radis);
ctx.closePath();
ctx.fill();
});
ctx.restore();
}
// 绘制图形
draw(ctx) {
ctx.save();
this.createPath(ctx);
ctx.stroke();
ctx.restore();
}
}
其他的图形,如果有不同于父类的方法可以在自身进行重写,具体重写方法可以看源代码。
事件与状态
图像标注工具主要存在三种事件:
- mousedown
- mousemove
- mouseup 图像标注工具主要存在四种状态:
- 绘制(DRAWING)
- 更新(UPDATING)
- 平移(MOVING)
- 默认(DEFAULT)
根据以上的三种事件和四种状态,在不同的事件和状态下执行不同的操作。这里对应源代码中的 canvasMousedown、canvasMousemove和canvasMouseup,这是图像标注工具的核心代码。