vue移动鼠标在canvas上画不规则图形(整合别人的,增加了一些功能)

发布时间 2023-10-24 13:42:43作者: yw3692582
1、draw_shape.js
  1 /**
  2  * 绘制不规则多边形
  3  */
  4 
  5 import { Message } from 'element-ui'
  6 
  7 export function draw_test(cav, list) {
  8  // 画布初始化
  9  let ctx = cav.getContext('2d')
 10  ctx.strokeStyle = 'red'
 11  ctx.lineWidth = 2
 12 
 13  // 变量初始化
 14  let isdraw = false // 是否在画图形
 15  let endtip = false // 是否结束一个多边形的绘制
 16  let coordinates = [] // 一个多边形信息
 17  let point_in_area = false // 鼠标是否在区域内
 18  let target_index = null // 目标索引
 19  let is_select = false // 是否选中某个区域
 20  let index_list = [] // 是否选中某个端点
 21  let is_click = false // 是否按下鼠标
 22  let click_point = {} // 按下鼠标的坐标
 23  // 初始判断是否有框
 24  draw_now_line(coordinates, ctx) // 把之前的点连线
 25  draw_now_circle(coordinates, ctx) //画之前的点
 26  if (list.length != 0) {
 27   // 画所有的
 28   draw_all_lines(list, ctx)
 29   draw_all_circles(list, ctx)
 30   fillarea(list, ctx)
 31  }
 32 
 33  //  鼠标按下
 34  cav.onmousedown = e => {
 35   if (e.which == 3) return
 36   let x = e.offsetX
 37   let y = e.offsetY
 38   //   是否在端点
 39   index_list = findClosestPointIndex(list, { x, y })
 40   if (index_list.length) {
 41    console.log('在端点')
 42    click_point = { x, y }
 43    is_click = true
 44    return
 45   }
 46   // 是否在区域内
 47   if (point_in_area) {
 48    ctx.clearRect(0, 0, 800, 600)
 49    draw_all_lines(list, ctx)
 50    draw_all_circles(list, ctx)
 51    fillarea(list, ctx, target_index)
 52    is_select = true
 53    move_draw(list, ctx, cav, target_index, x, y)
 54    return
 55   }
 56   is_select = false
 57   if (endtip) {
 58    if (list.length < 10) {
 59     endtip = false //清空,重新画
 60    } else {
 61     Message.warning('最多可绘制20个闭合的凸多边形区域')
 62     return
 63    }
 64   }
 65   //获取鼠标按下的坐标,放入数组中
 66   let insertFlag = true
 67   coordinates.forEach((item, index) => {
 68    if (item.x == x && item.y == y) {
 69     insertFlag = false
 70    }
 71   })
 72   if (insertFlag) {
 73    coordinates.push({ x: x, y: y })
 74    // 判断相交
 75    let lineList = []
 76    coordinates.forEach((item, index) => {
 77     if (index < coordinates.length - 1) {
 78      lineList.push([item.x, item.y, coordinates[index + 1].x, coordinates[index + 1].y])
 79     }
 80    })
 81    lineList.forEach((item, index) => {
 82     if (index > 0 && index < lineList.length - 1) {
 83      let flag = judgeIntersect(
 84       lineList[lineList.length - 1][0],
 85       lineList[lineList.length - 1][1],
 86       lineList[lineList.length - 1][2],
 87       lineList[lineList.length - 1][3],
 88       lineList[index - 1][0],
 89       lineList[index - 1][1],
 90       lineList[index - 1][2],
 91       lineList[index - 1][3]
 92      )
 93      if (flag) {
 94       coordinates.pop()
 95      }
 96     }
 97    })
 98   }
 99   draw_now_circle(coordinates, ctx)
100   fillarea(list, ctx)
101   isdraw = true //正在画多边形
102  }
103 
104  // 鼠标抬起
105  cav.onmouseup = e => {
106   console.log(e)
107   is_click = false
108   click_point = {}
109   index_list = []
110   console.log(point_in_area)
111   if (point_in_area) return
112   target_index = null
113   ctx.clearRect(0, 0, 800, 600) //清空画布
114   draw_now_line(coordinates, ctx) //把之前的点连线
115   draw_now_circle(coordinates, ctx) //画之前的点
116   if (list.length != 0) {
117    draw_all_lines(list, ctx)
118    draw_all_circles(list, ctx)
119    fillarea(list, ctx)
120   }
121  }
122 
123  // 鼠标移动
124  cav.onmousemove = e => {
125   // 判断是否移动到某个区域内
126   let x = e.offsetX
127   let y = e.offsetY
128   // 判断是否在某个区域内
129   let point = { x, y }
130   if (list.length) {
131    for (let i = 0; i < list.length; i++) {
132     point_in_area = isPointInPolygon(list[i], point)
133     if (point_in_area) {
134      target_index = i
135      break
136     }
137    }
138    cav.style.cursor = point_in_area ? 'move' : 'crosshair'
139   } else {
140    point_in_area = false
141    ctx.clearRect(0, 0, 800, 600) //清空画布
142    cav.style.cursor = 'crosshair'
143   }
144   if (point_in_area) {
145    // console.log(point_in_area, target_index)
146   } else {
147    is_select = false
148    target_index = null
149    ctx.clearRect(0, 0, 800, 600)
150    draw_all_lines(list, ctx)
151    draw_all_circles(list, ctx)
152    fillarea(list, ctx)
153   }
154   // 编辑端点判断是否在端点时点击并移动
155   if (list.length) {
156    // list_index 局部变量
157    let list_index = findClosestPointIndex(list, point)
158    if (list_index.length) changeDraw(list, cav, ctx, list_index)
159    // index_list 全局变量
160    if (index_list.length) {
161     let target_data = {
162      x: click_point.x + (x - click_point.x),
163      y: click_point.y + (y - click_point.y),
164     }
165     list[index_list[0]].splice(index_list[1], 1, target_data)
166     return
167    }
168   }
169   index_list = []
170   is_click = false
171   click_point = {}
172   //没开始画或者结束画之后不进行操作
173   if (coordinates.length == 0 || !isdraw || endtip) {
174    return
175   }
176   //获取上一个点
177   let last_x = coordinates[coordinates.length - 1].x
178   let last_y = coordinates[coordinates.length - 1].y
179   ctx.clearRect(0, 0, 800, 600) //清空画布
180   draw_now_line(coordinates, ctx) //把之前的点连线
181   draw_now_circle(coordinates, ctx) //画之前的点
182   if (list.length != 0) {
183    draw_all_lines(list, ctx)
184    draw_all_circles(list, ctx)
185    fillarea(list, ctx)
186   }
187   //获取鼠标移动时的点,画线,实现线段跟踪效果。
188   ctx.beginPath()
189   ctx.moveTo(last_x, last_y)
190   ctx.lineTo(x, y) //追踪鼠标
191   ctx.stroke() //绘制已定义的路径 追踪鼠标
192   ctx.closePath()
193  }
194 
195  // 鼠标双击
196  cav.ondblclick = e => {
197   if (point_in_area) return
198   if (coordinates.length <= 2) return Message.warning('至少需要三个点,请继续绘制')
199   //双击画布,在最后一个点的时候双击,自动连线第一个点,同时宣告画结束
200   let x0 = coordinates[0].x
201   let y0 = coordinates[0].y
202   let x1 = coordinates[coordinates.length - 1].x
203   let y1 = coordinates[coordinates.length - 1].y
204   ctx.beginPath()
205   ctx.moveTo(x0, y0)
206   ctx.lineTo(x1, y1)
207   ctx.stroke()
208   ctx.closePath()
209   isdraw = false
210   endtip = true
211   list.push(coordinates)
212   ctx.fillStyle = 'transparent'
213   let bx = coordinates[0].x
214   let by = coordinates[0].y
215   ctx.beginPath()
216   ctx.moveTo(bx, by)
217   for (let k = 1; k < coordinates.length; k++) {
218    let x = coordinates[k].x
219    let y = coordinates[k].y
220    ctx.lineTo(x, y)
221   }
222   ctx.fill()
223   ctx.closePath()
224   coordinates = []
225  }
226 
227  // 鼠标右键
228  cav.oncontextmenu = e => {
229   e.preventDefault()
230   coordinates = []
231   isdraw = false
232   endtip = true
233   clear_now_line(list, ctx)
234  }
235 
236  // 键盘事件(删除区域)
237  cav.onkeydown = k => {
238   let key = k.keyCode || k.which
239   if ([8, 46].includes(key)) {
240    if (!is_select) return Message.warning('请先点击某个区域为选中状态,再删除')
241    list.splice(target_index, 1)
242    ctx.clearRect(0, 0, 800, 600)
243    if (list.length != 0) {
244     draw_all_lines(list, ctx)
245     draw_all_circles(list, ctx)
246     fillarea(list, ctx)
247    } else {
248     ctx.clearRect(0, 0, 800, 600)
249    }
250   }
251  }
252 }
253 
254 /* 移动矩形 */
255 function move_draw(list, ctx, cav, index, sX, sY) {
256  let mark = list[index]
257  let handle_move = em => {
258   let target_data = []
259   mark.forEach(item => {
260    target_data.push({
261     x: item.x + (em.offsetX - sX),
262     y: item.y + (em.offsetY - sY),
263    })
264   })
265   ctx.clearRect(0, 0, 800, 600)
266   list.splice(index, 1, target_data)
267   draw_all_lines(list, ctx)
268   draw_all_circles(list, ctx)
269   fillarea(list, ctx, index)
270  }
271  let handle_up = e => {
272   cav.removeEventListener('mousemove', handle_move)
273  }
274  // 无脑多移除几次,不然会出bug
275  cav.removeEventListener('mousemove', handle_move) // 先移除监听
276  cav.removeEventListener('mousemove', handle_move) // 先移除监听
277  cav.removeEventListener('mouseup', handle_up) // 先移除监听
278  cav.removeEventListener('mouseup', handle_up) // 先移除监听
279  cav.removeEventListener('mousemove', handle_move) // 先移除监听
280  cav.removeEventListener('mousemove', handle_move) // 先移除监听
281  cav.removeEventListener('mouseup', handle_up) // 先移除监听
282  cav.removeEventListener('mouseup', handle_up) // 先移除监听
283  cav.addEventListener('mousemove', handle_move) // 再重新监听,解决点击多次移除不干净的bug
284  cav.addEventListener('mouseup', handle_up) // 再重新监听,解决点击多次移除不干净的bug
285 }
286 
287 /* 编辑多边形端点 */
288 function changeDraw(list, cav, ctx, index_list) {
289  cav.style.cursor = 'pointer'
290 }
291 
292 // 判断鼠标是否在端点
293 function findClosestPointIndex(list, points, type) {
294  let closestIndex = []
295  let closestDistance = Infinity
296  for (let i = 0; i < list.length; i++) {
297   for (let j = 0; j < list[i].length; j++) {
298    let point = list[i][j]
299    let diffX = Math.abs(points.x - point.x)
300    let diffY = Math.abs(points.y - point.y)
301    let distance = Math.sqrt(diffX * diffX + diffY * diffY)
302    if (distance <= 2 && distance < closestDistance) {
303     closestIndex = [i, j]
304     closestDistance = distance
305    }
306   }
307  }
308  if (type == 'is') return closestIndex.length
309  return closestIndex
310 }
311 
312 // 清除当前未画完成的区域
313 function clear_now_line(list, ctx) {
314  ctx.clearRect(0, 0, 800, 600)
315  if (list.length != 0) {
316   draw_all_lines(list, ctx)
317   draw_all_circles(list, ctx)
318   fillarea(list, ctx)
319  }
320 }
321 
322 // 绘制选中的,颜色填充
323 function fillarea(list, ctx, index) {
324  for (var i = 0; i < list.length; i++) {
325   var cors = list[i]
326   var x0 = cors[0].x
327   var y0 = cors[0].y
328   ctx.beginPath()
329   ctx.fillStyle = i == index ? 'rgba(255,0,0,0.1)' : 'transparent'
330   ctx.moveTo(x0, y0)
331   for (var j = 1; j < cors.length; j++) {
332    var x = cors[j].x
333    var y = cors[j].y
334    ctx.lineTo(x, y)
335   }
336   ctx.fill()
337   ctx.closePath()
338  }
339 }
340 
341 // 判断坐标点是否在区域内的函数
342 function isPointInPolygon(list, point) {
343  let isInside = false
344  // 射线与多边形边线的交点数量
345  let intersections = 0
346  for (let i = 0; i < list.length; i++) {
347   let coordinate1 = list[i]
348   let coordinate2 = list[(i + 1) % list.length]
349   // 判断射线与边线是否相交
350   if (
351    coordinate1.y > point.y !== coordinate2.y > point.y &&
352    point.x <
353     ((coordinate2.x - coordinate1.x) * (point.y - coordinate1.y)) /
354      (coordinate2.y - coordinate1.y) +
355      coordinate1.x
356   ) {
357    intersections++
358   }
359  }
360  // 判断点是否在多边形内
361  if (intersections % 2 !== 0) {
362   isInside = true
363  }
364  return isInside
365 }
366 
367 // 把当前绘制的多边形之前的坐标线段绘制出来
368 function draw_now_line(coordinates, ctx) {
369  for (let i = 0; i < coordinates.length - 1; i++) {
370   ctx.beginPath()
371   let x0 = coordinates[i].x
372   let y0 = coordinates[i].y
373   let x1 = coordinates[i + 1].x
374   let y1 = coordinates[i + 1].y
375   ctx.moveTo(x0, y0)
376   ctx.lineTo(x1, y1)
377   ctx.stroke()
378   ctx.closePath()
379  }
380 }
381 
382 // 把当前绘制的多边形之前的端点画圆
383 function draw_now_circle(coordinates, ctx) {
384  ctx.fillStyle = 'red'
385  for (let i = 0; i < coordinates.length; i++) {
386   let x = coordinates[i].x
387   let y = coordinates[i].y
388   ctx.beginPath() //起始一条路径,或重置当前路径
389   ctx.moveTo(x, y) //把路径移动到画布中的指定点(x,y)开始坐标
390   ctx.arc(x, y, 5, 0, Math.PI * 2) //
391   ctx.fill() //填充点
392   ctx.closePath() //创建从当前点回到起始点的路径
393  }
394 }
395 
396 // 画出所有多边形
397 function draw_all_lines(list, ctx) {
398  for (let i = 0; i < list.length; i++) {
399   let cors = list[i]
400   //前后坐标连线
401   for (let j = 0; j < cors.length - 1; j++) {
402    ctx.beginPath()
403    let x0 = cors[j].x
404    let y0 = cors[j].y
405    let x1 = cors[j + 1].x
406    let y1 = cors[j + 1].y
407    ctx.moveTo(x0, y0)
408    ctx.lineTo(x1, y1)
409    ctx.stroke()
410    ctx.closePath()
411   }
412   //最后一个与第一个连线
413   let begin_x = cors[0].x
414   let begin_y = cors[0].y
415   let end_x = cors[cors.length - 1].x
416   let end_y = cors[cors.length - 1].y
417   ctx.fillStyle = 'transparent'
418   ctx.beginPath()
419   ctx.moveTo(begin_x, begin_y)
420   ctx.lineTo(end_x, end_y)
421   ctx.stroke()
422   ctx.closePath()
423  }
424 }
425 
426 // 画出所有多边形的端点
427 function draw_all_circles(list, ctx, cav) {
428  ctx.fillStyle = 'red'
429  for (let i = 0; i < list.length; i++) {
430   let cors = list[i]
431   for (let j = 0; j < cors.length; j++) {
432    let x = cors[j].x
433    let y = cors[j].y
434    ctx.beginPath()
435    ctx.moveTo(x, y)
436    ctx.arc(x, y, 4, 0, Math.PI * 2)
437    ctx.fill()
438    ctx.closePath()
439   }
440  }
441 }
442 
443 // 判断直线是否相交
444 function judgeIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
445  if (
446   !(
447    Math.min(x1, x2) <= Math.max(x3, x4) &&
448    Math.min(y3, y4) <= Math.max(y1, y2) &&
449    Math.min(x3, x4) <= Math.max(x1, x2) &&
450    Math.min(y1, y2) <= Math.max(y3, y4)
451   )
452  )
453   return false
454  let u, v, w, z
455  u = (x3 - x1) * (y2 - y1) - (x2 - x1) * (y3 - y1)
456  v = (x4 - x1) * (y2 - y1) - (x2 - x1) * (y4 - y1)
457  w = (x1 - x3) * (y4 - y3) - (x4 - x3) * (y1 - y3)
458  z = (x2 - x3) * (y4 - y3) - (x4 - x3) * (y2 - y3)
459  return u * v <= 0.00000001 && w * z <= 0.00000001
460 }

2、vue界面中使用

  1 <template>
  2  <div>
  3   <div class="content">
  4    <img src="https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg" />
  5    <canvas id="mycanvas" ref="mycanvas" tabindex="0"></canvas>
  6   </div>
  7   <button @click="get_data">获取</button>
  8   <button @click="reload_dom">复原</button>
  9  </div>
 10 </template>
 11 
 12 <script>
 13 import { draw_test } from '../utils/test_draw_box'
 14 export default {
 15  inject: ['reload'],
 16  data() {
 17   return {
 18    isdraw: false, //是否在画图形
 19    coordinates: [], //一个多边形的坐标信息
 20    cor_index: 0, //当前多边形的索引
 21    endtip: false, //是否结束一个多边形的绘制
 22    //    mark_list: [], //所有多边形的信息
 23    mark_list: [
 24     [
 25      {
 26       x: 135,
 27       y: 49,
 28      },
 29      {
 30       x: 509,
 31       y: 50,
 32      },
 33      {
 34       x: 230,
 35       y: 198,
 36      },
 37     ],
 38     [
 39      {
 40       x: 536,
 41       y: 176,
 42      },
 43      {
 44       x: 318,
 45       y: 344,
 46      },
 47      {
 48       x: 615,
 49       y: 365,
 50      },
 51     ],
 52     [
 53      {
 54       x: 64,
 55       y: 357,
 56      },
 57      {
 58       x: 54,
 59       y: 527,
 60      },
 61      {
 62       x: 227,
 63       y: 524,
 64      },
 65      {
 66       x: 217,
 67       y: 362,
 68      },
 69     ],
 70    ], //所有多边形的信息
 71    PointInArea: false,
 72    index: null,
 73   }
 74  },
 75  mounted() {
 76   this.initCanvas()
 77  },
 78  methods: {
 79   /* 画布初始化 */
 80   initCanvas() {
 81    this.$nextTick(() => {
 82     // 初始化canvas宽高
 83     let cav = this.$refs.mycanvas
 84     cav.width = '800'
 85     cav.height = '600'
 86     let ctx = cav.getContext('2d')
 87     ctx.strokeStyle = 'red'
 88     cav.style.cursor = 'crosshair'
 89     // 计算使用变量
 90     let list = this.mark_list // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存
 91     // 若list长度不为0, 则显示已标记框
 92     draw_test(cav, list)
 93    })
 94   },
 95   get_data() {
 96    console.log(this.mark_list)
 97   },
 98   reload_dom() {
 99    this.reload()
100   },
101  },
102 }
103 </script>
104 <style lang="scss" scoped>
105 .content {
106  position: relative;
107  //  top: 50%;
108  //  left: 50%;
109  //  transform: translateX(-50%) translateX(-50%);
110  margin: 0 auto;
111  width: 800px;
112  height: 600px;
113 
114  img {
115   position: absolute;
116   top: 0;
117   left: 0;
118   width: 100%;
119   height: 100%;
120   z-index: 9;
121   user-select: none;
122  }
123 
124  canvas {
125   position: absolute;
126   top: 0;
127   left: 0;
128   z-index: 10;
129  }
130 }
131 // .main {
132 //  height: 90vh;
133 //  color: black;
134 //  background: white;
135 // }
136 // #mycanvas {
137 //  border: 1px solid red;
138 //  position: fixed;
139 //  left: 0;
140 //  right: 0;
141 //  margin: auto;
142 // }
143 </style>

界面效果:

参考地址1,画多边形:https://blog.csdn.net/m0_63853518/article/details/128301834
参考地址2,画矩形:https://blog.csdn.net/Sunshine_YXJ/article/details/108216971?spm=1001.2014.3001.5506