canvas数据标注功能简单实现:矩形、圆形

news2025/3/24 21:49:19

背景说明

基于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;
   }
 };
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2320358.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【UI设计】一些好用的免费图标素材网站

阿里巴巴矢量图标库https://www.iconfont.cn/国内最大的矢量图标库之一&#xff0c;拥有 800 万 图标资源。特色功能包括团队协作、多端适配、定制化编辑等&#xff0c;适合企业级项目、电商设计、中文产品开发等场景。IconParkhttps://iconpark.oceanengine.com/home字节跳动…

ubuntu 解挂载时提示 “umount: /home/xx/Applications/yy: target is busy.”

问题如题所示&#xff0c;我挂载一个squanfs文件系统到指定目录&#xff0c;当我使用完后&#xff0c;准备解挂载时&#xff0c;提示umount: /home/xx/Applications/yy: target is busy.&#xff0c;具体的如图所示&#xff0c; 这种提示通常是表明这个路径的内容正在被某些进…

一条不太简单的TEX学习之路

目录 rule raisebox \includegraphics newenviro 、\vspace \stretch \setlength 解释&#xff1a; 总结&#xff1a; 、\linespread newcommand \par 小四 \small simple 、mutiput画网格 解释&#xff1a; 图案解释&#xff1a; xetex pdelatex etc index 报…

Matplotlib完全指南:数据可视化从入门到实战

目录 引言 一、环境配置与基础概念 1.1 安装Matplotlib 1.2 导入惯例 1.3 两种绘图模式 二、基础图形绘制 2.1 折线图&#xff08;Line Plot&#xff09; 2.2 柱状图&#xff08;Bar Chart&#xff09; 三、高级图表类型 3.1 散点图&#xff08;Scatter Plot&#xff…

在大数据开发中ETL是指什么?

hello宝子们...我们是艾斯视觉擅长ui设计和前端数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩! 在数字经济时代&#xff0c;数据已成为企业最核心的资产。然而&#xff0c;分散在业务系统、日志文件…

OAuth 2.0认证

文章目录 1. 引言1.1 系列文章说明1.2 OAuth 2.0 的起源与演变1.3 应用场景概览 2. OAuth 2.0 核心概念2.1 角色划分2.2 核心术语解析 3. 四种授权模式详解3.1 授权码模式&#xff08;Authorization Code Grant&#xff09;3.1.1 完整流程解析3.1.2 PKCE 扩展&#xff08;防止授…

Kubernetes的Replica Set和ReplicaController有什么区别

ReplicaSet 和 ReplicationController 是 Kubernetes 中用于管理应用程序副本的两种资源&#xff0c;它们有类似的功能&#xff0c;但 ReplicaSet 是 ReplicationController 的增强版本。 以下是它们的主要区别&#xff1a; 1. 功能的演进 ReplicationController 是 Kubernete…

[Lc_2 二叉树dfs] 布尔二叉树的值 | 根节点到叶节点数字之和 | 二叉树剪枝

目录 1.计算布尔二叉树的值 题解 2.求根节点到叶节点数字之和 3. 二叉树剪枝 题解 1.计算布尔二叉树的值 链接&#xff1a;2331. 计算布尔二叉树的值 给你一棵 完整二叉树 的根&#xff0c;这棵树有以下特征&#xff1a; 叶子节点 要么值为 0 要么值为 1 &#xff0c;其…

蓝桥杯 之 第27场月赛总结

文章目录 习题1.抓猪拿国一2.蓝桥字符3.蓝桥大使4.拳头对决 习题 比赛地址 1.抓猪拿国一 十分简单的签到题 print(sum(list(range(17))))2.蓝桥字符 常见的字符匹配的问题&#xff0c;是一个二维dp的问题&#xff0c;转化为对应的动态规划求解 力扣的相似题目 可以关注灵神…

可视化动态表单动态表单界的天花板--Formily(阿里开源)

文章目录 1、Formily表单介绍2、安装依赖2.1、安装内核库2.2、 安装 UI 桥接库2.3、Formily 支持多种 UI 组件生态&#xff1a; 3、表单设计器3.1、核心理念3.2、安装3.3、示例源码 4、场景案例-登录注册4.1、Markup Schema 案例4.2、JSON Schema 案例4.3、纯 JSX 案例 1、Form…

Amdahl 定律

Amdahl 定律是用来表示&#xff0c;当提高系统某部分性能时对整个系统的影响&#xff0c;其公式如下&#xff1a; a表示我们提升部分初始耗时比例&#xff0c;k是我们的提升倍率&#xff0c;通过这个公式我们可以轻松的得知对每一部分的提醒&#xff0c;对整个系统带来的影响…

Linux系统之美:环境变量的概念以及基本操作

本节重点 理解环境变量的基本概念学会在指令和代码操作上查询更改环境变量环境变量表的基本概念父子进程间环境变量的继承与隔离 一、引入 1.1 自定义命令&#xff08;我们的exe&#xff09; 我们以往的Linux编程经验告诉我们&#xff0c;我们在对一段代码编译形成可执行文件后…

pnpm 报错 Error: Cannot find matching keyid 解决

1. 查看corepack版本&#xff0c;升级至0.31.0 npm i -g corepack0.31.0 这里注意环境变量&#xff0c;可能升级后还是指向旧版本&#xff0c;可以选择更新环境变量或者删除原指向的corepack命令 2. 更新pnpm corepack install -g pnpmlatest 问题解决。

Ubuntu实时读取音乐软件的音频流

文章目录 一. 前言二. 开发环境三. 具体操作四. 实际效果 一. 前言 起因是这样的&#xff0c;我需要在Ubuntu中&#xff0c;实时读取正在播放音乐的音频流&#xff0c;然后对音频进行相关的处理。本来打算使用的PipewireHelvum的方式实现&#xff0c;好处是可以直接利用Helvum…

Fiddler抓包工具最快入门

目录 前言 了解HTTP网络知识 简单了解网络访问过程 简单了解HTTP网络传输协议 工作过程 HTTP请求&#xff1a; Fildder工具使用教程 抓包的概念 一、什么是抓包 二、为什么要抓包 三、抓包的原理&#xff08;图解&#xff09; Fiddler工具 安装 使用 Fiddler查看…

编译器与中间表示:LLVM与GCC、G++、Clang的关系详解

编译器与中间表示&#xff1a;LLVM与GCC、G、Clang的关系详解 引言 编译器是软件开发中不可或缺的工具&#xff0c;它负责将高级语言&#xff08;如C/C、Java等&#xff09;转换为机器语言&#xff0c;使计算机能够理解和执行程序。中间表示&#xff08;Intermediate Represe…

股指期货贴水波动,影响哪些投资策略?

先来说说“贴水”。简单来说&#xff0c;贴水就是股指期货的价格比现货价格低。比如&#xff0c;沪深300指数现在是4000点&#xff0c;但股指期货合约的价格只有3950点&#xff0c;这就叫贴水。贴水的大小会影响很多投资策略的收益&#xff0c;接下来我们就来看看具体的影响。 …

RHCE 使用nginx搭建网站

一。准备工作 Windows dns映射 创建目录网页 vim 编辑内容 添加如下 重启nginx服务&#xff0c;在Windows浏览器进行测试

AtCoder Beginner Contest 398(ABCDEF)

A - Doors in the Center 翻译&#xff1a; 找到一个满足下面情况长为N的字符串&#xff1a; 每个字符是 - 或 。是一个回文。包含一个或两个 。如果包含两个相邻的 。 如此字符串为独一无二的。 思路&#xff1a; 从两端使用 开始构造回文。在特判下中间部分&#xff0c;…

单表达式倒计时工具:datetime的极度优雅(智普清言)

一个简单表达式&#xff0c;也可以优雅自成工具。 笔记模板由python脚本于2025-03-22 20:25:49创建&#xff0c;本篇笔记适合任意喜欢学习的coder翻阅。 【学习的细节是欢悦的历程】 博客的核心价值&#xff1a;在于输出思考与经验&#xff0c;而不仅仅是知识的简单复述。 Pyth…