canvas数据标注功能简单实现:矩形、圆形
背景说明
基于UI同学的设计,在市面上找不到刚刚好的数据标注工具,遂决定自行开发。目前需求是实现图片的矩形、圆形标注,并获取标注的坐标信息,使用canvas可以比较方便的实现该功能。
主要功能
选中图形,进行拖动
使用事件监听,mousedown确认鼠标按下坐标是否在图形内,mousemove提供坐标来更新图形坐标。
选中小圆点,进行缩放
使用事件监听,mousedown确认鼠标按下坐标是否在小圆点内,mousemove提供坐标来更新图形宽高(或半径)。
基础方法
// 获取canvas元素
const canvas = document.getElemnetById("canvas") as HtmlCanvasElement;
// 获取canvas上下文
const ctx = canvas.getContent("2d");
// 画矩形
ctx.beginPath();
ctx.rect(x, y, w, h); // 参数:左上角坐标x,y 宽高wh
ctx.fillStyle = color1; // 填充色
ctx.strokeStyle = color2; // 线条色
ctx.lineWidth = width1; // 线条宽
ctx.fill(); // 填充
ctx.stroke(); // 画线
ctx.closePath();
// 画圆形
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2); // 参数:中心点坐标x,y 半径2 从0角度绘制到Math.PI*2角度
ctx.fillStyle = color1;
ctx.strokeStyle = color2;
ctx.lineWidth = width1;
ctx.fill();
ctx.stroke();
ctx.closePath();
主要方法
新增图形
定义图形类,以矩形为例
export class Rectangle {
x!: number;
y!: number;
w!: number;
h!: number;
fillColor!: string;
strokeColor!: string;
strokeWidth!: number;
selectedDotIndex = -1;
uuid!: string;
isInsideDotFlag = false;
dotR = 7;
dotLineWidth = 2;
dotFillColor = '#3D7FFF';
dotStrokeColor = '#FFFFFF';
dotConnectLineStorkeColor = '#3D7FFF';
dotConnectLineWidth = 1;
// 最小11x11
minSize = 11;
constructor(x: number, y: number, w: number, h: number, fillColor: string, storkeColor: string, storkeWidth: number) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.fillColor = fillColor;
this.strokeColor = storkeColor;
this.strokeWidth = storkeWidth;
this.uuid = this.generateUUID();
}
// 小圆点坐标
get dots() {
return [
[this.x, this.y],
[this.x + this.w / 2, this.y],
[this.x + this.w, this.y],
[this.x + this.w, this.y + this.h / 2],
[this.x + this.w, this.y + this.h],
[this.x + this.w / 2, this.y + this.h],
[this.x, this.y + this.h],
[this.x, this.y + this.h / 2]
];
}
// 是否在小圆点内
isInsideDot(x: number, y: number) {
for (let index = 0; index < this.dots.length; index++) {
const [dx, dy] = this.dots[index];
if (this.distanceBetweenTwoPoints(x, y, dx, dy) < this.dotR) {
this.selectedDotIndex = index;
return true;
}
}
this.selectedDotIndex = -1;
return false;
}
// 是否在图形内
isInsideShape(x: number, y: number) {
this.isInsideDotFlag = this.isInsideDot(x, y);
if (this.isInsideDotFlag) { // 在小圆点内也算在图形内
return true;
}
return x >= this.x && x <= (this.x + this.w) && y >= this.y && y <= (this.y + this.h);
}
// 更新小圆点
updateDot(mouseX: number, mouseY: number) {
const handleIndex = this.selectedDotIndex;
switch (handleIndex) {
case 0: // 左上角
this.w = this.x + this.w - mouseX;
this.h = this.y + this.h - mouseY;
this.x = mouseX;
this.y = mouseY;
break;
case 1: // 上边中点
this.h = this.y + this.h - mouseY;
this.y = mouseY;
break;
case 2: // 右上角
this.w = mouseX - this.x;
this.h = this.y + this.h - mouseY;
this.y = mouseY;
break;
case 3: // 右边中点
this.w = mouseX - this.x;
break;
case 4: // 右下角
this.w = mouseX - this.x;
this.h = mouseY - this.y;
break;
case 5: // 下边中点
this.h = mouseY - this.y;
break;
case 6: // 左下角
this.w = this.x + this.w - mouseX;
this.h = mouseY - this.y;
this.x = mouseX;
break;
case 7: // 左边中点
this.w = this.x + this.w - mouseX;
this.x = mouseX;
break;
default:
break;
}
this.w = Math.max(this.w, this.minSize);
this.h = Math.max(this.h, this.minSize);
}
// 更新坐标
updateXy(mouseX: number, mouseY: number) {
this.x = mouseX - this.w / 2;
this.y = mouseY - this.h / 2;
}
// 画小圆点
drawDots(ctx: CanvasRenderingContext2D) {
for (let index = 0; index < this.dots.length; index++) {
const [x1, y1] = this.dots[index];
const [x2, y2] = this.dots[(index + 1) % this.dots.length];
// 点
ctx.beginPath();
ctx.arc(x1, y1, this.dotR, 0, Math.PI * 2);
ctx.fillStyle = this.dotFillColor;
ctx.strokeStyle = this.dotStrokeColor;
ctx.lineWidth = this.dotLineWidth;
ctx.fill();
ctx.stroke();
ctx.closePath();
// 线
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = this.dotConnectLineStorkeColor;
ctx.lineWidth = this.dotConnectLineWidth;
ctx.fill();
ctx.stroke();
ctx.closePath();
}
}
// 画图
draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.rect(this.x, this.y, this.w, this.h);
ctx.fillStyle = this.fillColor;
ctx.strokeStyle = this.strokeColor;
ctx.lineWidth = this.strokeWidth;
ctx.fill();
ctx.stroke();
ctx.closePath();
}
// 获取uuid
generateUUID() {
var random = Math.random().toString(36).substring(2);
var timestamp = new Date().getTime().toString(36);
return random + timestamp;
}
// 计算2点之间的距离
distanceBetweenTwoPoints(x1: number, y1: number, x2: number, y2: number) {
const xDiff = x1 - x2;
const yDiff = y1 - y2;
return Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2));
}
// 获取左上、右下坐标
getPoints() {
return [this.x, this.y, this.x + this.w, this.y + this.h];
}
}
添加图形:把图形对象加入列表,方便管理
const rect = new Rectangle(this.startX, this.startY, 120, 120, this.hexToRgba(color, 0.3), color, 3);
this.dataList.unshift({
name: '矩形',
type: 'Rectangle',
color,
shape: rect
});
绘制图形:根据列表进行绘制
reDraw() {
// 清空画布
this.ctxFront.clearRect(0, 0, this.canvasFrontEle.width, this.canvasFrontEle.height);
for (let index = 0; index < this.dataList.length; index++) {
const shape = this.dataList[index].shape;
shape.draw(this.ctxFront);
// 选中的图形绘制小圆点
if (this.selectedShape && shape.uuid === this.selectedShape.uuid) {
shape.drawDots(this.ctxFront);
}
}
}
事件监听
// 监听canvas事件
listenCanvas() {
// 事件监听
// 鼠标按下
this.canvasFrontEle.onmousedown = (e) => {
const rect = this.canvasFrontEle.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
for (let index = 0; index < this.dataList.length; index++) {
const shape = this.dataList[index].shape;
if (shape.isInsideShape(clickX, clickY)) {
this.selectedShape = shape;
break;
} else if (index === this.dataList.length - 1) {
this.selectedShape = null;
}
}
if (this.selectedShape) { // 选中图形
if (this.selectedShape.isInsideDotFlag) { // 在顶点
window.onmousemove = (e) => {
const moveX = e.clientX - rect.left;
const moveY = e.clientY - rect.top;
this.selectedShape!.updateDot(moveX, moveY);
this.reDraw();
};
} else { // 移动
window.onmousemove = (e) => {
const moveX = e.clientX - rect.left;
const moveY = e.clientY - rect.top;
this.selectedShape!.updateXy(moveX, moveY);
this.reDraw();
};
}
}
this.reDraw();
window.onmouseup = (e) => {
window.onmousemove = null;
window.onmouseup = null;
}
};
}