首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 25-04-24 17:01
  • 3504
  • 7763
blog.csdn.net

效果:

页面描述:

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

实现思路:

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

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

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

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

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

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

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

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

  1. <template>
  2. <div class="allbody">
  3. <div class="body-top">
  4. <button class="top-item2" @click="clearAnnotations">清空button>
  5. div>
  6. <div class="body-btn">
  7. <div class="btn-content">
  8. <div class="image-container">
  9. <img :src="state.imageUrls[state.currentPage - 1]" @mousedown="startAnnotation" @mousemove="updateAnnotation" @mouseup="endAnnotation" />
  10. <canvas ref="annotationCanvas">canvas>
  11. <div v-for="annotation in annotations[state.currentPage - 1]" :key="annotation.id" class="annotation" :style="annotationStyle(annotation)">
  12. <div class="label">{{ annotation.label }}div>
  13. div>
  14. div>
  15. <Pagination
  16. v-model:current="state.currentPage"
  17. v-model:page-size="state.pageSize"
  18. show-quick-jumper
  19. :total="state.imageUrls.length"
  20. :showSizeChanger="false"
  21. :show-total="total => `共 ${total} 张`" />
  22. div>
  23. <div class="sidebar">
  24. <div class="sidebar-title">标签div>
  25. <div class="tags">
  26. <div class="tags-item" v-for="(tags, index2) in state.tagsList" :key="index2" @click="checkTag(index2)">
  27. <div class="tags-checkbox">
  28. <div :class="tags.check === true ? 'checkbox-two' : 'notcheckbox-two'">div>
  29. div>
  30. <div class="tags-right">
  31. <input class="tags-color" type="color" v-model="tags.color" />
  32. <input type="type" class="tags-input" v-model="tags.name" />
  33. <button class="tags-not" @click="deleteTag(index2)"><DeleteOutlined style="color: #ff0202" />button>
  34. div>
  35. div>
  36. div>
  37. <div class="sidebar-btn">
  38. <button class="btn-left" @click="addTags()">添加button>
  39. div>
  40. <div class="sidebar-title">数据div>
  41. <div class="sidebars">
  42. <div class="sidebar-item" v-for="(annotation, index) in annotations[state.currentPage - 1]" :key="annotation.id">
  43. <div class="sidebar-item-font">{{ index + 1 }}.{{ annotation.name }}div>
  44. <button class="sidebar-item-icon" @click="removeAnnotation(annotation.id)"><DeleteOutlined style="color: #ff0202" />button>
  45. >div>
  46. div>
  47. div>
  48. div>
  49. template>
  50. <script lang="ts" setup>
  51. import { DeleteOutlined } from '@ant-design/icons-vue';
  52. import { Pagination } from 'ant-design-vue';
  53. interface State {
  54. tagsList: any;
  55. canvasX: number;
  56. canvasY: number;
  57. currentPage: number;
  58. pageSize: number;
  59. imageUrls: string[];
  60. };
  61. const state = reactive<State>({
  62. tagsList: [], // 标签列表
  63. canvasX: 0,
  64. canvasY: 0,
  65. currentPage: 1,
  66. pageSize: 1,
  67. imageUrls: [apiUrl.value + '/api/File/Image/annexpic/20241203Q9NHJ.jpg', apiUrl.value + '/api/file/Image/document/20241225QBYXZ.jpg'],
  68. });
  69. interface Annotation {
  70. id: string;
  71. name: string;
  72. x: number;
  73. y: number;
  74. width: number;
  75. height: number;
  76. color: string;
  77. label: string;
  78. border: string;
  79. };
  80. const annotations = reactive<Array<Annotation[]>>([[]]);
  81. let currentAnnotation: Annotation | null = null;
  82. //开始标注
  83. function startAnnotation(event: MouseEvent) {
  84. // 获取当前选中的标签
  85. var tagsCon = { id: 1, check: true, color: '#000000', name: '安全帽' };
  86. // 遍历标签列表,获取当前选中的标签
  87. for (var i = 0; i < state.tagsList.length; i++) {
  88. if (state.tagsList[i].check) {
  89. tagsCon.id = state.tagsList[i].id;
  90. tagsCon.check = state.tagsList[i].check;
  91. tagsCon.color = state.tagsList[i].color;
  92. tagsCon.name = state.tagsList[i].name;
  93. }
  94. }
  95. // 创建新的标注
  96. currentAnnotation = {
  97. id: crypto.randomUUID(),
  98. name: tagsCon.name,
  99. x: event.offsetX,
  100. y: event.offsetY,
  101. width: 0,
  102. height: 0,
  103. color: '#000000',
  104. label: (annotations[state.currentPage - 1].length || 0) + 1 + tagsCon.name,
  105. border: tagsCon.color,
  106. };
  107. annotations[state.currentPage - 1].push(currentAnnotation);
  108. //记录鼠标按下的位置
  109. state.canvasX = event.offsetX;
  110. state.canvasY = event.offsetY;
  111. //监听鼠标如果是按下后马上抬起,结束标注
  112. const mouseupHandler = () => {
  113. endAnnotation();
  114. window.removeEventListener('mouseup', mouseupHandler);
  115. };
  116. window.addEventListener('mouseup', mouseupHandler);
  117. }
  118. //更新标注
  119. function updateAnnotation(event: MouseEvent) {
  120. if (currentAnnotation) {
  121. //更新当前标注的宽高,为负数时,鼠标向左或向上移动
  122. currentAnnotation.width = event.offsetX - currentAnnotation.x;
  123. currentAnnotation.height = event.offsetY - currentAnnotation.y;
  124. }
  125. //如果正在绘制中,更新临时矩形的位置
  126. if (annotationCanvas.value) {
  127. const canvas = annotationCanvas.value;
  128. //取得类名为image-container的div的宽高
  129. const imageContainer = document.querySelector('.image-container');
  130. canvas.width = imageContainer?.clientWidth || 800;
  131. canvas.height = imageContainer?.clientHeight || 534;
  132. const context = canvas.getContext('2d');
  133. if (context) {
  134. context.clearRect(0, 0, canvas.width, canvas.height);
  135. context.strokeStyle = currentAnnotation?.border || '#000000';
  136. context.lineWidth = 2;
  137. context.strokeRect(state.canvasX, state.canvasY, currentAnnotation?.width || 0, currentAnnotation?.height || 0);
  138. }
  139. }
  140. }
  141. function endAnnotation() {
  142. //刷新annotations[state.currentPage - 1],触发重新渲染
  143. annotations[state.currentPage - 1] = annotations[state.currentPage - 1].slice();
  144. currentAnnotation = null;
  145. }
  146. function annotationStyle(annotation: Annotation) {
  147. //如果宽高为负数,需要调整left和top的位置
  148. const left = annotation.width < 0 ? annotation.x + annotation.width : annotation.x;
  149. const top = annotation.height < 0 ? annotation.y + annotation.height : annotation.y;
  150. return {
  151. left: `${left}px`,
  152. top: `${top}px`,
  153. width: `${Math.abs(annotation.width)}px`,
  154. height: `${Math.abs(annotation.height)}px`,
  155. border: `2px solid ${annotation.border}`,
  156. };
  157. }
  158. // 选择标签
  159. function checkTag(index2: number) {
  160. state.tagsList.forEach((item, index) => {
  161. if (index === index2) {
  162. item.check = true;
  163. } else {
  164. item.check = false;
  165. }
  166. });
  167. }
  168. // 删除标签
  169. function deleteTag(index: number) {
  170. state.tagsList.splice(index, 1);
  171. }
  172. function addTags() {
  173. state.tagsList.push({ id: state.tagsList.length + 1, check: false, color: '#000000', name: '' });
  174. }
  175. // 移除某个标注
  176. function removeAnnotation(id: string) {
  177. const index = annotations[state.currentPage - 1].findIndex(a => a.id === id);
  178. if (index !== -1) {
  179. annotations[state.currentPage - 1].splice(index, 1);
  180. }
  181. }
  182. // 清空所有标注
  183. function clearAnnotations() {
  184. annotations[state.currentPage - 1].splice(0, annotations[state.currentPage - 1].length);
  185. }
  186. onMounted(() => {
  187. for (let i = 0; i < state.imageUrls.length; i++) {
  188. annotations.push([]);
  189. }
  190. });
  191. script>
  192. <style>
  193. .body-top {
  194. display: flex;
  195. flex-direction: row;
  196. align-items: center;
  197. justify-content: center;
  198. margin-bottom: 10px;
  199. width: 85%;
  200. }
  201. .top-item1 {
  202. width: 70px;
  203. height: 28px;
  204. line-height: 26px;
  205. text-align: center;
  206. background-color: #028dff;
  207. border: 1px solid #028dff;
  208. border-radius: 5px;
  209. font-size: 14px;
  210. color: #fff;
  211. margin-left: 20px;
  212. }
  213. .top-item2 {
  214. width: 70px;
  215. height: 28px;
  216. line-height: 26px;
  217. text-align: center;
  218. background-color: rgb(255, 2, 2);
  219. border: 1px solid rgb(255, 2, 2);
  220. border-radius: 5px;
  221. font-size: 14px;
  222. color: #fff;
  223. margin-left: 20px;
  224. }
  225. .body-btn {
  226. margin: 0;
  227. padding: 10px 13px 0 0;
  228. min-height: 630px;
  229. display: flex;
  230. background-color: #f5f5f5;
  231. }
  232. .btn-content {
  233. flex-grow: 1;
  234. padding: 10px;
  235. box-sizing: border-box;
  236. display: flex;
  237. flex-direction: column;
  238. align-items: center;
  239. }
  240. .image-container {
  241. height: 500px;
  242. margin: 40px;
  243. }
  244. .image-container img {
  245. height: 500px !important;
  246. }
  247. .ant-pagination {
  248. margin-bottom: 18px;
  249. }
  250. .number-input {
  251. width: 70px;
  252. border: 1px solid #ccc;
  253. border-radius: 4px;
  254. text-align: center;
  255. font-size: 16px;
  256. background-color: #f9f9f9;
  257. outline: none;
  258. color: #66afe9;
  259. }
  260. .sidebar {
  261. display: flex;
  262. flex-direction: column;
  263. width: 280px;
  264. height: 640px;
  265. background-color: #fff;
  266. padding: 10px;
  267. border-radius: 7px;
  268. }
  269. .sidebar-title {
  270. font-size: 16px;
  271. font-weight: 600;
  272. margin-bottom: 10px;
  273. }
  274. .sidebars {
  275. overflow: auto;
  276. }
  277. .sidebar .tags {
  278. margin-bottom: 10px;
  279. }
  280. .tags-item {
  281. display: flex;
  282. flex-direction: row;
  283. align-items: center;
  284. }
  285. .tags-checkbox {
  286. width: 24px;
  287. height: 24px;
  288. border-radius: 50px;
  289. border: 1px solid #028dff;
  290. display: flex;
  291. flex-direction: column;
  292. align-items: center;
  293. justify-content: center;
  294. margin-right: 7px;
  295. }
  296. .checkbox-two {
  297. background-color: #028dff;
  298. width: 14px;
  299. height: 14px;
  300. border-radius: 50px;
  301. }
  302. .notcheckbox-two {
  303. width: 14px;
  304. height: 14px;
  305. border-radius: 50px;
  306. border: 1px solid #028dff;
  307. }
  308. .tags-right {
  309. display: flex;
  310. flex-direction: row;
  311. align-items: center;
  312. background-color: #f5f5f5;
  313. border-radius: 5px;
  314. padding: 5px;
  315. width: 90%;
  316. }
  317. .tags-color {
  318. width: 26px;
  319. height: 26px;
  320. border-radius: 5px;
  321. }
  322. .tags-input {
  323. border: 1px solid #fff;
  324. width: 153px;
  325. margin: 0 10px;
  326. }
  327. .tags-not {
  328. border: 1px solid #f5f5f5;
  329. font-size: 12px;
  330. }
  331. .sidebar-btn {
  332. display: flex;
  333. flex-direction: row;
  334. align-items: center;
  335. justify-content: right;
  336. }
  337. .btn-left {
  338. width: 60px;
  339. height: 28px;
  340. line-height: 26px;
  341. text-align: center;
  342. border: 1px solid #028dff;
  343. border-radius: 5px;
  344. font-size: 14px;
  345. color: #028dff;
  346. }
  347. .btn-right {
  348. width: 60px;
  349. height: 28px;
  350. line-height: 26px;
  351. text-align: center;
  352. background-color: #028dff;
  353. border: 1px solid #028dff;
  354. border-radius: 5px;
  355. font-size: 14px;
  356. color: #fff;
  357. margin-left: 10px;
  358. }
  359. .sidebar-item {
  360. display: flex;
  361. justify-content: space-between;
  362. align-items: center;
  363. padding-right: 2px;
  364. }
  365. .sidebar-item-font {
  366. margin-right: 10px;
  367. }
  368. .sidebar-item-icon {
  369. font-size: 12px;
  370. border: 1px solid #fff;
  371. }
  372. .image-annotator {
  373. display: flex;
  374. height: 100%;
  375. }
  376. .image-container {
  377. flex: 1;
  378. position: relative;
  379. overflow: auto;
  380. }
  381. .image-container img {
  382. max-width: 100%;
  383. height: auto;
  384. }
  385. .annotation {
  386. position: absolute;
  387. box-sizing: border-box;
  388. }
  389. canvas {
  390. position: absolute;
  391. top: 0;
  392. left: 0;
  393. width: 100%;
  394. height: 100%;
  395. pointer-events: none; /* 防止遮挡鼠标事件 */
  396. }
  397. style>

注:本文转载自blog.csdn.net的悠悠:)的文章"https://blog.csdn.net/weixin_66549669/article/details/144984878"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

138
3C硬件
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top