前端 图片上鼠标画矩形框,标注文字,任意删除

效果:

页面描述:

对给定的几张图片,每张能用鼠标在图上画框,标注相关文字,框的颜色和文字内容能自定义改变,能删除任意画过的框。

实现思路:

1、对给定的这几张图片,用分页器绑定展示,能选择图片;

2、图片上绑定事件@mousedown鼠标按下——开始画矩形、@mousemove鼠标移动——绘制中临时画矩形、@mouseup鼠标抬起——结束画矩形重新渲染;

开始画矩形:鼠标按下,记录鼠标按下的位置。遍历标签数组,找到check值为true的标签,用其样式和名字创建新的标签,加入该图片的矩形框们的数组。注意,监听鼠标如果是按下后马上抬起,结束标注。

更新矩形:识别到新的标签存在,鼠标移动时监听移动距离,更新当前矩形宽高,用canvas绘制实时临时矩形。

结束画矩形:刷新该图片的矩形框们的数组,触发重新渲染。

3、在图片上v-for遍历渲染矩形框,盒子绑定动态样式改变宽高;

4、右侧能添加、修改矩形框颜色和文字;

5、列举出每个矩形框名称,能选择进行删除,还能一次清空;

<template>
<div class="allbody">
      <div class="body-top">
        <button class="top-item2" @click="clearAnnotations">清空</button>
      </div>
      <div class="body-btn">
        <div class="btn-content">
          <div class="image-container">
            <!-- <img :src="imageUrl" alt="Character Image" /> -->
            <img :src="state.imageUrls[state.currentPage - 1]" @mousedown="startAnnotation" @mousemove="updateAnnotation" @mouseup="endAnnotation" />
            <!-- 使用canvas覆盖在图片上方,用于绘制临时矩形 -->
            <canvas ref="annotationCanvas"></canvas>
            <div v-for="annotation in annotations[state.currentPage - 1]" :key="annotation.id" class="annotation" :style="annotationStyle(annotation)">
              <div class="label">{{ annotation.label }}</div>
            </div>
          </div>
          <Pagination
            v-model:current="state.currentPage"
            v-model:page-size="state.pageSize"
            show-quick-jumper
            :total="state.imageUrls.length"
            :showSizeChanger="false"
            :show-total="total => `共 ${total} 张`" />
        </div>
        <div class="sidebar">
          <div class="sidebar-title">标签</div>
          <div class="tags">
            <div class="tags-item" v-for="(tags, index2) in state.tagsList" :key="index2" @click="checkTag(index2)">
              <div class="tags-checkbox">
                <div :class="tags.check === true ? 'checkbox-two' : 'notcheckbox-two'"></div>
              </div>
              <div class="tags-right">
                <input class="tags-color" type="color" v-model="tags.color" />
                <input type="type" class="tags-input" v-model="tags.name" />
                <button class="tags-not" @click="deleteTag(index2)"><DeleteOutlined style="color: #ff0202" /></button>
              </div>
            </div>
          </div>
          <div class="sidebar-btn">
            <button class="btn-left" @click="addTags()">添加</button>
          </div>
          <div class="sidebar-title">数据</div>
          <div class="sidebars">
            <div class="sidebar-item" v-for="(annotation, index) in annotations[state.currentPage - 1]" :key="annotation.id">
              <div class="sidebar-item-font">{{ index + 1 }}.{{ annotation.name }}</div>
              <button class="sidebar-item-icon" @click="removeAnnotation(annotation.id)"><DeleteOutlined style="color: #ff0202" /></button> </div
          ></div>
        </div>
      </div>
    </div>
</template>
<script lang="ts" setup>
  import { DeleteOutlined } from '@ant-design/icons-vue';
  import { Pagination } from 'ant-design-vue';

  interface State {
    tagsList: any;
    canvasX: number;
    canvasY: number;
    currentPage: number;
    pageSize: number;
    imageUrls: string[];
  };

  const state = reactive<State>({
    tagsList: [], // 标签列表
    canvasX: 0,
    canvasY: 0,
    currentPage: 1,
    pageSize: 1,
    imageUrls: [apiUrl.value + '/api/File/Image/annexpic/20241203Q9NHJ.jpg', apiUrl.value + '/api/file/Image/document/20241225QBYXZ.jpg'],
  });

  interface Annotation {
    id: string;
    name: string;
    x: number;
    y: number;
    width: number;
    height: number;
    color: string;
    label: string;
    border: string;
  };

  const annotations = reactive<Array<Annotation[]>>([[]]);
  let currentAnnotation: Annotation | null = null;

  //开始标注
  function startAnnotation(event: MouseEvent) {
    // 获取当前选中的标签
    var tagsCon = { id: 1, check: true, color: '#000000', name: '安全帽' };
    // 遍历标签列表,获取当前选中的标签
    for (var i = 0; i < state.tagsList.length; i++) {
      if (state.tagsList[i].check) {
        tagsCon.id = state.tagsList[i].id;
        tagsCon.check = state.tagsList[i].check;
        tagsCon.color = state.tagsList[i].color;
        tagsCon.name = state.tagsList[i].name;
      }
    }
    // 创建新的标注
    currentAnnotation = {
      id: crypto.randomUUID(),
      name: tagsCon.name,
      x: event.offsetX,
      y: event.offsetY,
      width: 0,
      height: 0,
      color: '#000000',
      label: (annotations[state.currentPage - 1].length || 0) + 1 + tagsCon.name,
      border: tagsCon.color,
    };
    annotations[state.currentPage - 1].push(currentAnnotation);

    //记录鼠标按下的位置
    state.canvasX = event.offsetX;
    state.canvasY = event.offsetY;

    //监听鼠标如果是按下后马上抬起,结束标注
    const mouseupHandler = () => {
      endAnnotation();
      window.removeEventListener('mouseup', mouseupHandler);
    };
    window.addEventListener('mouseup', mouseupHandler);
  }

  //更新标注
  function updateAnnotation(event: MouseEvent) {
    if (currentAnnotation) {
      //更新当前标注的宽高,为负数时,鼠标向左或向上移动
      currentAnnotation.width = event.offsetX - currentAnnotation.x;
      currentAnnotation.height = event.offsetY - currentAnnotation.y;
    }

    //如果正在绘制中,更新临时矩形的位置
    if (annotationCanvas.value) {
      const canvas = annotationCanvas.value;
      //取得类名为image-container的div的宽高
      const imageContainer = document.querySelector('.image-container');
      canvas.width = imageContainer?.clientWidth || 800;
      canvas.height = imageContainer?.clientHeight || 534;
      const context = canvas.getContext('2d');
      if (context) {
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.strokeStyle = currentAnnotation?.border || '#000000';
        context.lineWidth = 2;
        context.strokeRect(state.canvasX, state.canvasY, currentAnnotation?.width || 0, currentAnnotation?.height || 0);
      }
    }
  }

  function endAnnotation() {
    //刷新annotations[state.currentPage - 1],触发重新渲染
    annotations[state.currentPage - 1] = annotations[state.currentPage - 1].slice();
    currentAnnotation = null;
  }

  function annotationStyle(annotation: Annotation) {
    //如果宽高为负数,需要调整left和top的位置
    const left = annotation.width < 0 ? annotation.x + annotation.width : annotation.x;
    const top = annotation.height < 0 ? annotation.y + annotation.height : annotation.y;
    return {
      left: `${left}px`,
      top: `${top}px`,
      width: `${Math.abs(annotation.width)}px`,
      height: `${Math.abs(annotation.height)}px`,
      border: `2px solid ${annotation.border}`,
    };
  }

  // 选择标签
  function checkTag(index2: number) {
    state.tagsList.forEach((item, index) => {
      if (index === index2) {
        item.check = true;
      } else {
        item.check = false;
      }
    });
  }

  // 删除标签
  function deleteTag(index: number) {
    state.tagsList.splice(index, 1);
  }

  function addTags() {
    state.tagsList.push({ id: state.tagsList.length + 1, check: false, color: '#000000', name: '' });
  }

  // 移除某个标注
  function removeAnnotation(id: string) {
    const index = annotations[state.currentPage - 1].findIndex(a => a.id === id);
    if (index !== -1) {
      annotations[state.currentPage - 1].splice(index, 1);
    }
  }

  // 清空所有标注
  function clearAnnotations() {
    annotations[state.currentPage - 1].splice(0, annotations[state.currentPage - 1].length);
  }

  onMounted(() => {
    for (let i = 0; i < state.imageUrls.length; i++) {
      annotations.push([]);
    }
  });

</script>
<style>
  .body-top {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    margin-bottom: 10px;
    width: 85%;
  }
  .top-item1 {
    width: 70px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    background-color: #028dff;
    border: 1px solid #028dff;
    border-radius: 5px;
    font-size: 14px;
    color: #fff;
    margin-left: 20px;
  }
  .top-item2 {
    width: 70px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    background-color: rgb(255, 2, 2);
    border: 1px solid rgb(255, 2, 2);
    border-radius: 5px;
    font-size: 14px;
    color: #fff;
    margin-left: 20px;
  }
  .body-btn {
    margin: 0;
    padding: 10px 13px 0 0;
    min-height: 630px;
    display: flex;
    background-color: #f5f5f5;
  }
  .btn-content {
    flex-grow: 1;
    padding: 10px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .image-container {
    height: 500px;
    margin: 40px;
  }
  .image-container img {
    height: 500px !important;
  }
  .ant-pagination {
    margin-bottom: 18px;
  }
  .number-input {
    width: 70px;
    border: 1px solid #ccc;
    border-radius: 4px;
    text-align: center;
    font-size: 16px;
    background-color: #f9f9f9;
    outline: none;
    color: #66afe9;
  }
  .sidebar {
    display: flex;
    flex-direction: column;
    width: 280px;
    height: 640px;
    background-color: #fff;
    padding: 10px;
    border-radius: 7px;
  }
  .sidebar-title {
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 10px;
  }
  .sidebars {
    overflow: auto;
  }
  .sidebar .tags {
    margin-bottom: 10px;
  }
  .tags-item {
    display: flex;
    flex-direction: row;
    align-items: center;
  }
  .tags-checkbox {
    width: 24px;
    height: 24px;
    border-radius: 50px;
    border: 1px solid #028dff;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-right: 7px;
  }
  .checkbox-two {
    background-color: #028dff;
    width: 14px;
    height: 14px;
    border-radius: 50px;
  }
  .notcheckbox-two {
    width: 14px;
    height: 14px;
    border-radius: 50px;
    border: 1px solid #028dff;
  }
  .tags-right {
    display: flex;
    flex-direction: row;
    align-items: center;
    background-color: #f5f5f5;
    border-radius: 5px;
    padding: 5px;
    width: 90%;
  }
  .tags-color {
    width: 26px;
    height: 26px;
    border-radius: 5px;
  }
  .tags-input {
    border: 1px solid #fff;
    width: 153px;
    margin: 0 10px;
  }
  .tags-not {
    border: 1px solid #f5f5f5;
    font-size: 12px;
  }
  .sidebar-btn {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: right;
  }
  .btn-left {
    width: 60px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    border: 1px solid #028dff;
    border-radius: 5px;
    font-size: 14px;
    color: #028dff;
  }
  .btn-right {
    width: 60px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    background-color: #028dff;
    border: 1px solid #028dff;
    border-radius: 5px;
    font-size: 14px;
    color: #fff;
    margin-left: 10px;
  }
  .sidebar-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-right: 2px;
  }
  .sidebar-item-font {
    margin-right: 10px;
  }
  .sidebar-item-icon {
    font-size: 12px;
    border: 1px solid #fff;
  }

  .image-annotator {
    display: flex;
    height: 100%;
  }

  .image-container {
    flex: 1;
    position: relative;
    overflow: auto;
  }

  .image-container img {
    max-width: 100%;
    height: auto;
  }

  .annotation {
    position: absolute;

    box-sizing: border-box;
  }

  canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; /* 防止遮挡鼠标事件 */
  }
</style>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

shell练习

1、shell 脚本写出检测 /tmp/size.log 文件如果存在显示它的内容&#xff0c;不存在则创建一个文件将创建时间写入。 2、写一个 shel1 脚本,实现批量添加 20个用户,用户名为user01-20,密码为user 后面跟5个随机字符。 3、编写个shel 脚本将/usr/local 日录下大于10M的文件转移…

day01-HTML-CSS——基础标签样式表格标签表单标签

目录 此篇为简写笔记下端1-3为之前笔记&#xff08;强迫症、保证文章连续性&#xff09;完整版笔记代码模仿新浪新闻首页完成审核不通过发不出去HTMLCSS1 HTML1.1 介绍1.1.1 WebStrom中基本配置 1.2 快速入门1.3 基础标签1.3.1 标题标签1.3.2 hr标签1.3.3 字体标签1.3.4 换行标…

大疆上云API连接遥控器和无人机

文章目录 1、部署大疆上云API关于如何连接我们自己部署的上云API2、开启无人机和遥控器并连接自己部署的上云API如果遥控器和无人机没有对频的情况下即只有遥控器没有无人机的情况下如果遥控器和无人机已经对频好了的情况下 4、订阅无人机或遥控器的主题信息4.1、订阅无人机实时…

如何用 SSH 访问 QNX 虚拟机

QNX 虚拟机默认是开启 SSH 服务的&#xff0c;如果要用 SSH 访问 QNX 虚拟机&#xff0c;就需要知道虚拟机的 IP 地址&#xff0c;用户和密码。本文我们来看看如何获取这些参数。 1. 启动虚拟机 启动过程很慢&#xff0c;请耐心等待。 2. 查看 IP 地址 等待 IDE 连接到虚拟机。…

【Vue + Antv X6】可拖拽流程图组件

使用事项&#xff1a; ❗先放个组件上来&#xff0c;使用手册有空会补全 ❗需要下载依赖 “antv/x6”: “^2.18.1”, “antv/x6-plugin-dnd”: “^2.1.1”, 组件&#xff1a; 组件使用&#xff1a; <flowChart :key"flowChartKey" ref"flowChart" lef…

在线或离线llama.cpp安装和模型启动

该版本安装时间是2025-01-10&#xff0c;因为不同版本可能安装上会有所不同&#xff0c;下面也会讲到。 先说下问题——按照官方文档找不到执行命令llama-cli或./llama-cli 先附上llama.cpp的github地址&#xff1a;https://github.com/ggerganov/llama.cpp&#xff0c;build…

一个运行在浏览器中的开源Web操作系统Puter本地部署与远程访问

文章目录 前言1.关于Puter2.本地部署Puter3.Puter简单使用4. 安装内网穿透5.配置puter公网地址6. 配置固定公网地址 &#x1f4a1; 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击跳转到网站…

上市公司专利数据、专利申请、专利授权和质量指标计算(1990-2022年)-社科数据

上市公司专利数据、专利申请、专利授权和质量指标计算&#xff08;1990-2022年&#xff09;-社科数据https://download.csdn.net/download/paofuluolijiang/90028569 https://download.csdn.net/download/paofuluolijiang/90028569 专利数据作为衡量企业创新能力和技术实力的…

js:事件流

事件流 事件流是指事件完整执行过程中的流动路径 一个事件流需要经过两个阶段&#xff1a;捕获阶段&#xff0c;冒泡阶段 捕获阶段是在dom树里获取目标元素的过程&#xff0c;从大到小 冒泡阶段是获取以后回到开始&#xff0c;从小到大&#xff0c;像冒泡一样 实际开发中大…

嵌入式入门Day38

C Day1 第一个C程序C中的输入输出输出操作coutcin练习 命名空间使用方法自定义命名空间冲突问题 C对字符串的扩充C风格字符串的使用定义以及初始化C风格字符串与C风格字符串的转换C风格的字符串的关系运算常用的成员变量输入方法 布尔类型C对堆区空间使用的扩充作业 第一个C程序…

FFmpeg音视频流媒体,视频编解码性能优化

你是不是也有过这样一个疑问&#xff1a;视频如何从一个简单的文件变成你手机上快速播放的短片&#xff0c;或者是那种占满大屏幕的超高清大片&#xff1f;它背后的法宝&#xff0c;离不开一个神奇的工具——FFmpeg&#xff01;说它强大&#xff0c;完全不为过&#xff0c;它在…

LIO-SAM代码解析:mapOptmization.cpp(一)

文章目录 主流程1. loopInfoHandler1.1 updateInitialGuess1.2 extractSurroundingKeyFrames1.3 downsampleCurrentScan1.4 scan2MapOptimization1.5 saveKeyFramesAndFactor1.6 correctPoses1.7 publishOdometry 1.8 publishFrames 主流程 1. loopInfoHandler 1.1 updateInit…

Django学习笔记之数据库(一)

文章目录 安装一、数据库配置二、基本操作步骤1.增加2.查看3.排序4.更新5.删除数据 三、一对多&#xff0c;多对多&#xff0c;一对一1.一对多1.一对一1.多对多 四、查询操作五、聚合操作六、F和Q操作 安装 首先就是安装Mysql和Navicat。 一、数据库配置 其实整个就是连接前端…

《分布式光纤传感:架设于桥梁监测领域的 “智慧光网” 》

桥梁作为交通基础设施的重要组成部分&#xff0c;其结构健康状况直接关系到交通运输的安全和畅通。随着桥梁建设规模的不断扩大和服役年限的增长&#xff0c;桥梁结构的安全隐患日益凸显&#xff0c;传统的监测方法已难以满足对桥梁结构健康实时、全面、准确监测的需求。分布式…

什么是顶级思维?

在现代社会&#xff0c;我们常常听到“顶级思维”这个概念&#xff0c;但究竟什么才是顶级思维&#xff1f;它又是如何影响一个人的成功和幸福呢&#xff1f;今天&#xff0c;我们就来探讨一下顶级思维的几个关键要素&#xff0c;并分享一些实用的生活哲学。 1. 身体不适&…

更新Office后,LabVIEW 可执行程序生成失败

问题描述&#xff1a; 在计算机中&#xff0c;LabVIEW 开发的源程序运行正常&#xff0c;但在生成可执行程序时提示以下错误&#xff1a; ​ A VI broke during the build process from being saved without a block diagram. Either open the build specification to include…

Domain Adaptation(李宏毅)机器学习 2023 Spring HW11 (Boss Baseline)

1. 领域适配简介 领域适配是一种迁移学习方法,适用于源领域和目标领域数据分布不同但学习任务相同的情况。具体而言,我们在源领域(通常有大量标注数据)训练一个模型,并希望将其应用于目标领域(通常只有少量或没有标注数据)。然而,由于这两个领域的数据分布不同,模型在…

25年无人机行业资讯 | 1.1 - 1.5

25年无人机行业资讯 | 1.1 - 1.5 中央党报《经济日报》刊文&#xff1a;低空经济蓄势待发&#xff0c;高质量发展需的平衡三大关系 据新华网消息&#xff0c;2025年1月3日&#xff0c;中央党报《经济日报》发表文章指出&#xff0c;随着国家发展改革委低空经济发展司的成立&a…

AI刷题-数位长度筛选问题、数值生成与运算问题

目录 一、数位长度筛选问题 问题描述 测试样例 解题思路&#xff1a; 问题理解 数据结构选择 算法步骤 关键点 最终代码&#xff1a; 运行结果&#xff1a; 二、数值生成与运算问题 问题描述 测试样例 解题思路&#xff1a; 问题理解 数据结构选择 算法步骤…

Qiskit快速编程探索(进阶篇)

五、量子电路模拟:探索量子世界的虚拟实验室 5.1 Aer模拟器:强大的模拟引擎 在量子计算的探索旅程中,Aer模拟器作为Qiskit的核心组件之一,宛如一座功能强大的虚拟实验室,为开发者提供了在经典计算机上模拟量子电路运行的卓越能力。它打破了硬件条件的限制,使得研究者无…