vue3通过canvas实现图片圈选功能

 更新时间:2023年12月08日 09:01:32   作者:闪现上空篮  
这篇文章将给大家详细介绍了vue3如何通过canvas实现图片圈选功能,文中的示例代码讲解详细,具有一定的参考价值,感兴趣的小伙伴快来跟随小编一起学习一下吧

canvas实现圈选

具体效果

思路

  • 容器里包裹着一张图片和一个canvas, 让其同等大小,在图片加载完成后获取到图片大小再设置canvas大小。
  • 要能拖动, 需要设置定位,要实现绘制,所以canvas要置于图片上层,通过z-index设置,两种功能不能同时实现,需要通过按钮开启。
  • 实现交点处按钮拖拽重绘,此处的点不能使用canvas绘制,canvas绘制不具备DOM元素无法添加事件,此处可以通过DOM来绘制交点实心圆。为实心圆添加移动等等事件,拖动重绘,此处要注意,拖动重绘的时候不要重绘交点,不然会拖动一次后移动事件就会失效。
  • 选中删除, 通过canvas的isPointInPath方法来进行判断,若是选中点存在绘制图形择重绘。
  • 通过监听touchStart是否存在两个触摸点来实现图片的手势放大缩小。

思路1

页面加载完之后,设置canvas大小,如果存在圈选图则绘制,同时为容器添加touch事件用于双指缩小放大。

 nextTick(() => {
    let imgRef = img.value
    let map = 'https://z1.ax1x.com/2023/12/07/pigCCPH.png'
    imgRef.setAttribute('src', map)
    imgRef.onload = () => {
      let height = imgRef.offsetHeight
      let width = imgRef.offsetWidth
      imgHeight.value = height
      let canvasRef = canvas.value
      let imgWrapRef = imgWrap.value

      canvasRef.setAttribute('width', width)
      canvasRef.setAttribute('height', height)

      imgWrapRef.style.width = width + 'px'
      imgWrapRef.style.height = height + 'px'
      canvasObj.value = canvasRef.getContext('2d')
      canvasObj.value.lineWidth = 1
      canvasObj.value.strokeStyle = '#687072'

      //绘制已保存的图
      drawList()
      reset()

      nextTick(() => {
        zoomInOut()
      })
    }
  })

思路2 & 思路3

根据标识判断是绘制还是拖动图片, 拖动的情况下判断是不是点击了交点,如果是交点就拖动交点重绘,如果不是交点就拖动图片。如果是绘制则每次点的时候都绘制一个实心圆并添加相应拖动事件,绘制情况下到达设置点个数或者交点位置相近则自动闭合图形。

// 绘制圆点
function drawCircle(left: number, top: number, color: string) {
  let pointDom = document.createElement('div')
  pointDom.setAttribute('class', 'point')
  let style = `background-color:${color};
                 left:${left}px;
                 top:${top}px;
                 width: 30px;
                 height: 30px;
                 border-radius: 50%;
                 position: absolute;
                 touch-action: none;
                 z-index: 2;
                 transform: translate(-50%, -50%);`
  pointDom.setAttribute('style', style)

  const move = (e: any) => {
    let oldLeft = +pointDom.style.left.slice(0, -2)
    let oldTop = +pointDom.style.top.slice(0, -2)
    let left = oldLeft - (movePoint.value.x - e.pageX)
    let top = oldTop - (movePoint.value.y - e.pageY)

    movePoint.value = {
      x: e.pageX,
      y: e.pageY
    }

    pointDom.style.left = `${left}px`
    pointDom.style.top = `${top}px`

    const setPosition = (list: any) => {
      list.some((item: any) => {
        return item.some((it: any) => {
          let isX = ~~it.x <= ~~oldLeft + 3 && ~~it.x >= ~~oldLeft - 3
          let isY = ~~it.y <= ~~oldTop + 3 && ~~it.y >= ~~oldTop - 3
          if (isX && isY) {
            it.x = left
            it.y = top
            return true
          }
          return false
        })
      })
    }

    setPosition(sweepList.value)
    setPosition(delList.value)

    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      drawList({ point: { x: 0, y: 0 }, resetPoint: false })
    }, 5)
    e.preventDefault()
  }

  pointDom.onpointerdown = (e: any) => {
    movePoint.value = {
      x: e.pageX,
      y: e.pageY
    }
    e.stopPropagation()
    if (openDraw.value) {
      if (pointList.value.length > 2) {
        closeFigure()
      }
      return
    }
    pointDom.addEventListener('pointermove', move)
  }

  pointDom.onpointerup = () => {
    if (!openDraw.value) {
      drawList()
    }
    pointDom.removeEventListener('pointermove', move)
  }

  pointDom.onpointerleave = () => {
    pointDom.removeEventListener('pointermove', move)
  }
  imgWrap.value.appendChild(pointDom)
}

// 绘制图形
function drawList(params: listType = { point: { x: 0, y: 0 }, resetPoint: true }) {
  if (params.resetPoint) {
    let pointDoms = Array.from(document.getElementsByClassName('point'))
    pointDoms.forEach((item) => {
      imgWrap.value.removeChild(item)
    })
  }

  canvasObj.value.clearRect(0, 0, img.value.offsetWidth, img.value.offsetHeight)
  try {
    sweepList.value.forEach((item, i) => {
      drawPic(item, 'rgba(29,179,219,0.4)')
      if (
        params.point.x != 0 &&
        params.point.y != 0 &&
        canvasObj.value.isPointInPath(params.point.x, params.point.y)
      ) {
        if (!!delList.value.length) {
          sweepList.value.push(delList.value[0])
        }
        delList.value = sweepList.value.splice(i, 1)
        emits('update:list', sweepList.value)
        throw new Error()
      }

      if (params.resetPoint) {
        item.forEach((subItem: Point) => {
          drawCircle(subItem.x, subItem.y, 'rgb(0,180,226)')
        })
      }
    })

    delList.value.forEach((item) => {
      drawPic(item, 'rgba(233,79,79, 0.5)')
      if (
        params.point.x != 0 &&
        params.point.y != 0 &&
        canvasObj.value.isPointInPath(params.point.x, params.point.y)
      ) {
        let temp = { ...item }
        sweepList.value.push(temp)
        delList.value = []
        emits('update:list', sweepList.value)
        throw new Error()
      }

      if (params.resetPoint) {
        item.forEach((subItem: Point) => {
          drawCircle(subItem.x, subItem.y, 'rgb(233,79,79)')
        })
      }
    })
  } catch (e) {
    drawList()
  }
}

function drawPic(item: any, bgColor: string) {
  canvasObj.value.fillStyle = bgColor
  canvasObj.value.beginPath()
  canvasObj.value.moveTo(item[0].x, item[0].y)
  item.forEach((subItem: Point, index: number) => {
    if (index > 0) {
      canvasObj.value.lineTo(subItem.x, subItem.y)
      canvasObj.value.stroke()
    }
  })
  canvasObj.value.closePath()
  canvasObj.value.stroke()
  canvasObj.value.fill()
}

思路4

每次点击的时候记录点下的坐标点,当是拖动模式下并且点下与弹起是的坐标点相同,则认为是选绘制图形操作,判断这个坐标点是否存在于canvas绘制的图形上,存在则选中重绘。

   // 记录当前点击坐标
  let pointDown = {
    x: e.offsetX,
    y: e.offsetY
  }
  if (!openDraw.value) {
    curPoint.value = pointDown
  }
  
   // 记录当前点击坐标, 用于判断是否为选中区域, 用于处理选中删除
  if (!openDraw.value) {
    if (e.offsetX == curPoint.value.x && e.offsetY == curPoint.value.y) {
      drawList({ point: { x: e.offsetX, y: e.offsetY }, resetPoint: true })
    }
  }
     
  // 判断传入坐标是否在canvas上
  canvasObj.value.isPointInPath(params.point.x, params.point.y)

思路5

双指放大和缩小, 记录第一次按下两点间的距离,监听移动事件,记录新的距离,计算两个距离之间的倍数关系, 通过当前倍数做限制,最后通过scale实现图片的放大缩小。

// 双指放大缩小
let initialDistance = 0
const ctTouchStart = (event: any) => {
  if (event.touches.length == 2) {
    let touch1 = event.touches[0]
    let touch2 = event.touches[1]
    initialDistance = Math.sqrt(
      Math.pow(touch1.pageX - touch2.pageX, 2) + Math.pow(touch1.pageY - touch2.pageY, 2)
    )
  }
}

const ctTouchMove = (event: any) => {
  if (event.touches.length == 2) {
    let touch1 = event.touches[0]
    let touch2 = event.touches[1]
    let distance = Math.sqrt(
      Math.pow(touch1.pageX - touch2.pageX, 2) + Math.pow(touch1.pageY - touch2.pageY, 2)
    )
    let scale = distance / initialDistance

    if (currentSize.value * scale >= 5) {
      currentSize.value = 5
    } else if (currentSize.value * scale <= 1) {
      currentSize.value = 1
    } else {
      currentSize.value = currentSize.value * scale
    }
    img.value.style.transform = 'scale(' + currentSize.value + ')'
  }
}

const ctTouchEnd = () => {
  initialDistance = 0
}

function zoomInOut() {
  let ctRef = imgWrap.value
  ctRef.addEventListener('touchstart', ctTouchStart)
  ctRef.addEventListener('touchmove', ctTouchMove)
  ctRef.addEventListener('touchend', ctTouchEnd)
}

function removeZoomInOut() {
  let ctRef = imgWrap.value
  ctRef.removeEventListener('touchstart', ctTouchStart)
  ctRef.removeEventListener('touchmove', ctTouchMove)
  ctRef.removeEventListener('touchend', ctTouchEnd)
}

具体代码如下

<template>
  <div class="area-conatiner">
    <div class="canvas-wrap" ref="canvasWrap">
      <div
        ref="imgWrap"
        class="modal-img-wrap"
        @pointerdown="mousedown($event)"
        @pointerup="mouseup($event)"
        @pointerleave="mouseup($event)"
      >
        <canvas class="canvas" ref="canvas"></canvas>
        <img ref="img" class="modal-img" />
      </div>
      <div class="action-btn">
        <div class="action-item location" @click="drawArea">
          <img :src="enableImg" alt="" />
        </div>
        <div class="action-item location" v-if="!!delList.length" @click="delArea">
          <img :src="getImage(`area/delete`)" alt="" />
        </div>
      </div>

      <div class="action-btn map-set">
        <div class="action-item location" @click="drawAreaSet('1')">
          <img :src="getImage('area/enlarged')" alt="" />
        </div>
        <div class="action-item location" @click="drawAreaSet('2')">
          <img :src="getImage('area/narrow')" alt="" />
        </div>
        <div class="action-item location" @click="drawAreaSet('3')">
          <img :src="getImage('area/reset')" alt="" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, onMounted, computed, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import {
  checkPointCross,
  checkPointConcave,
  checkPointClose,
  getImage
} from '@/utils/auxiliaryFunc'

interface Point {
  x: number
  y: number
}

interface listType {
  point: Point
  resetPoint: boolean
}

const imgWrap = ref()
const canvas = ref()
const img = ref()
const canvasWrap = ref()
const mousedownEvent = ref()

//画图
let openDraw = ref(false)
let rectList = ref([])
let pointList = ref<Array<Point>>([])
let canvasObj = ref<any>()
let maxPointNum = ref(6)
let minPointNum = ref(3)
let sweepList = ref<Array<Array<Point>>>([])
let delList = ref<Array<Array<Point>>>([])
let imgHeight = ref(0) //图片高度
let openEnable = ref(false)
let currentSize = ref(1)
let curPoint = ref<Point>({ x: 0, y: 0 })
let movePoint = ref({ x: 0, y: 0 })
let timer: NodeJS.Timeout

const emits = defineEmits<{
  (e: 'update:list', val: Array<Array<Point>>): void
}>()

onMounted(() => {
  initArea()
})

onBeforeUnmount(() => {
  removeZoomInOut()
})

const enableImg = computed(() => {
  let imgUrl = openEnable.value ? 'openEnabled' : 'enabled'
  return getImage(`area/${imgUrl}`)
})

//区域选择
function initArea() {
  rectList.value = []
  nextTick(() => {
    let imgRef = img.value
    let map = 'https://z1.ax1x.com/2023/12/07/pigCCPH.png'
    imgRef.setAttribute('src', map)
    imgRef.onload = () => {
      let height = imgRef.offsetHeight
      let width = imgRef.offsetWidth
      imgHeight.value = height
      let canvasRef = canvas.value
      let imgWrapRef = imgWrap.value

      canvasRef.setAttribute('width', width)
      canvasRef.setAttribute('height', height)

      imgWrapRef.style.width = width + 'px'
      imgWrapRef.style.height = height + 'px'
      canvasObj.value = canvasRef.getContext('2d')
      canvasObj.value.lineWidth = 1
      canvasObj.value.strokeStyle = '#687072'

      //绘制已保存的图
      drawList()
      reset()

      nextTick(() => {
        zoomInOut()
      })
    }
  })
}

let initialDistance = 0
const ctTouchStart = (event: any) => {
  if (event.touches.length == 2) {
    let touch1 = event.touches[0]
    let touch2 = event.touches[1]
    initialDistance = Math.sqrt(
      Math.pow(touch1.pageX - touch2.pageX, 2) + Math.pow(touch1.pageY - touch2.pageY, 2)
    )
  }
}

const ctTouchMove = (event: any) => {
  if (event.touches.length == 2) {
    let touch1 = event.touches[0]
    let touch2 = event.touches[1]
    let distance = Math.sqrt(
      Math.pow(touch1.pageX - touch2.pageX, 2) + Math.pow(touch1.pageY - touch2.pageY, 2)
    )
    let scale = distance / initialDistance

    if (currentSize.value * scale >= 5) {
      currentSize.value = 5
    } else if (currentSize.value * scale <= 1) {
      currentSize.value = 1
    } else {
      currentSize.value = currentSize.value * scale
    }
    img.value.style.transform = 'scale(' + currentSize.value + ')'
  }
}

const ctTouchEnd = () => {
  initialDistance = 0
}

// 双指放大缩小
function zoomInOut() {
  let ctRef = imgWrap.value
  ctRef.addEventListener('touchstart', ctTouchStart)
  ctRef.addEventListener('touchmove', ctTouchMove)
  ctRef.addEventListener('touchend', ctTouchEnd)
}

function removeZoomInOut() {
  let ctRef = imgWrap.value
  ctRef.removeEventListener('touchstart', ctTouchStart)
  ctRef.removeEventListener('touchmove', ctTouchMove)
  ctRef.removeEventListener('touchend', ctTouchEnd)
}

// 绘制圆点
function drawCircle(left: number, top: number, color: string) {
  let pointDom = document.createElement('div')
  pointDom.setAttribute('class', 'point')
  let style = `background-color:${color};
                 left:${left}px;
                 top:${top}px;
                 width: 30px;
                 height: 30px;
                 border-radius: 50%;
                 position: absolute;
                 touch-action: none;
                 z-index: 2;
                 transform: translate(-50%, -50%);`
  pointDom.setAttribute('style', style)

  const move = (e: any) => {
    let oldLeft = +pointDom.style.left.slice(0, -2)
    let oldTop = +pointDom.style.top.slice(0, -2)
    let left = oldLeft - (movePoint.value.x - e.pageX)
    let top = oldTop - (movePoint.value.y - e.pageY)

    movePoint.value = {
      x: e.pageX,
      y: e.pageY
    }

    pointDom.style.left = `${left}px`
    pointDom.style.top = `${top}px`

    const setPosition = (list: any) => {
      list.some((item: any) => {
        return item.some((it: any) => {
          let isX = ~~it.x <= ~~oldLeft + 3 && ~~it.x >= ~~oldLeft - 3
          let isY = ~~it.y <= ~~oldTop + 3 && ~~it.y >= ~~oldTop - 3
          if (isX && isY) {
            it.x = left
            it.y = top
            return true
          }
          return false
        })
      })
    }

    setPosition(sweepList.value)
    setPosition(delList.value)

    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      drawList({ point: { x: 0, y: 0 }, resetPoint: false })
    }, 5)
    e.preventDefault()
  }

  pointDom.onpointerdown = (e: any) => {
    movePoint.value = {
      x: e.pageX,
      y: e.pageY
    }
    e.stopPropagation()
    if (openDraw.value) {
      if (pointList.value.length > 2) {
        closeFigure()
      }
      return
    }
    pointDom.addEventListener('pointermove', move)
  }

  pointDom.onpointerup = () => {
    if (!openDraw.value) {
      drawList()
    }
    pointDom.removeEventListener('pointermove', move)
  }

  pointDom.onpointerleave = () => {
    pointDom.removeEventListener('pointermove', move)
  }
  imgWrap.value.appendChild(pointDom)
}

function mousedown(e: any) {
  if (e.button === 2) {
    return false
  }

  mousedownEvent.value = e
  // 图片拖拽
  let imgWrapRef = imgWrap.value
  let pointDown = {
    x: e.offsetX,
    y: e.offsetY
  }

  // 记录当前点击坐标
  if (!openDraw.value) {
    curPoint.value = pointDown
  }

  let x = e.pageX - imgWrapRef.offsetLeft
  let y = e.pageY - imgWrapRef.offsetTop

  let move = (e: any) => {
    let imgWidth = imgWrapRef.offsetWidth * currentSize.value
    let imgHeight = imgWrapRef.offsetHeight * currentSize.value
    let leftWidth = e.pageX - x,
      topWidth = e.pageY - y

    imgWrapRef.style.left = leftWidth + 'px'
    imgWrapRef.style.top = topWidth + 'px'

    // 解决边界拖出问题
    let canvasWrapWidth = canvasWrap.value.offsetWidth
    let canvasWrapHeight = canvasWrap.value.offsetHeight

    if (imgWidth >= canvasWrapWidth) {
      if (leftWidth >= 0) {
        imgWrapRef.style.left = '0px'
      } else if (leftWidth + imgWidth <= canvasWrapWidth) {
        imgWrapRef.style.left = canvasWrapWidth - imgWidth + 1 + 'px'
      }
    }

    if (imgHeight >= canvasWrapHeight) {
      if (topWidth >= 0) {
        imgWrapRef.style.top = '0px'
      } else if (topWidth + imgHeight <= canvasWrapHeight) {
        imgWrapRef.style.top = canvasWrapHeight - imgHeight + 'px'
      }
    }
  }

  if (openDraw.value) {
    let pointColor = 'rgba(0,180,226)'
    if (pointList.value.length === 0) {
      drawCircle(pointDown.x, pointDown.y, pointColor)
      canvasObj.value.beginPath()
      canvasObj.value.moveTo(pointDown.x, pointDown.y)
    } else {
      const check = checkPointClose(pointDown, pointList.value, minPointNum.value)
      if (check == 'closeFirst') {
        closeFigure()
        return
      }
      if (!check) {
        return
      }

      drawCircle(pointDown.x, pointDown.y, pointColor)
      // 已经有点了,连成线
      canvasObj.value.beginPath()
      let lastPoint = pointList.value.slice(-1)[0]
      canvasObj.value.moveTo(lastPoint.x, lastPoint.y)
      canvasObj.value.lineTo(pointDown.x, pointDown.y)
      canvasObj.value.stroke()
    }
    pointList.value.push({
      ...pointDown
    })
    // 如果已经到达最大数量,则直接闭合图形
    if (pointList.value.length >= maxPointNum.value) {
      closeFigure()
      return
    }
    e.preventDefault()
  } else {
    //图片拖拽
    e.preventDefault()
    // 添加指针移动事件
    imgWrapRef.addEventListener('pointermove', move)

    // 添加指针抬起事件,鼠标抬起,将事件移除
    imgWrapRef.addEventListener('pointerup', () => {
      imgWrapRef.removeEventListener('pointermove', move)
    })
    // 指针离开父级元素,把事件移除
    imgWrapRef.addEventListener('pointerleave', () => {
      imgWrapRef.removeEventListener('pointermove', move)
    })
  }
}

function mouseup(e: any) {
  // 记录当前点击坐标, 用于判断是否为选中区域, 用于处理选中删除
  if (!openDraw.value) {
    if (e.offsetX == curPoint.value.x && e.offsetY == curPoint.value.y) {
      drawList({ point: { x: e.offsetX, y: e.offsetY }, resetPoint: true })
    }
  }
}

// 闭合图型
function closeFigure() {
  // 检查部分
  if (!checkPointCross(pointList.value[0], pointList.value)) {
    ElMessage.error('闭合图形时发生横穿线,请重新绘制!')
    clear()
    return
  }
  if (!checkPointConcave(pointList.value[0], pointList.value, true)) {
    ElMessage.error('闭合图形时出现凹多边形,请重新绘制!')
    clear()
    return
  }
  if (pointList.value.length >= minPointNum.value) {
    // 符合要求
    canvasObj.value.fillStyle = 'rgba(29,179,219,0.4)'
    for (let i = 0; i < pointList.value.length - 2; i++) {
      canvasObj.value.lineTo(pointList.value[i].x, pointList.value[i].y)
    }
    canvasObj.value.closePath()
    canvasObj.value.stroke()
    canvasObj.value.fill()

    sweepList.value.push(pointList.value)
    emits('update:list', sweepList.value)
    openEnable.value = false

    pointList.value = []
    openDraw.value = false
    canvas.value.style.cursor = 'move'
  } else {
    ElMessage.error('最低绘制3个点!')
  }
}

function clear() {
  drawList()
  openEnable.value = false
  pointList.value = []
  openDraw.value = false
  canvas.value.style.cursor = 'move'
}

function drawArea() {
  if (sweepList.value.length === 5) {
    ElMessage.error('最多选择5个区域')
    return false
  }

  if (openEnable.value && pointList.value.length < 3) {
    pointList.value = []
  }
  if (pointList.value.length > 2) {
    closeFigure()
  }
  openEnable.value = !openEnable.value

  if (openEnable.value) {
    openDraw.value = true
    canvas.value.style.cursor = 'crosshair'
  } else {
    openDraw.value = false
    canvas.value.style.cursor = 'move'
    clear()
  }
}

// 绘制单个图形
function drawPic(item: any, bgColor: string) {
  canvasObj.value.fillStyle = bgColor
  canvasObj.value.beginPath()
  canvasObj.value.moveTo(item[0].x, item[0].y)
  item.forEach((subItem: Point, index: number) => {
    if (index > 0) {
      canvasObj.value.lineTo(subItem.x, subItem.y)
      canvasObj.value.stroke()
    }
  })
  canvasObj.value.closePath()
  canvasObj.value.stroke()
  canvasObj.value.fill()
}

//重新绘制成功的区域图
function drawList(params: listType = { point: { x: 0, y: 0 }, resetPoint: true }) {
  if (params.resetPoint) {
    let pointDoms = Array.from(document.getElementsByClassName('point'))
    pointDoms.forEach((item) => {
      imgWrap.value.removeChild(item)
    })
  }

  canvasObj.value.clearRect(0, 0, img.value.offsetWidth, img.value.offsetHeight)
  try {
    sweepList.value.forEach((item, i) => {
      drawPic(item, 'rgba(29,179,219,0.4)')
      if (
        params.point.x != 0 &&
        params.point.y != 0 &&
        canvasObj.value.isPointInPath(params.point.x, params.point.y)
      ) {
        if (!!delList.value.length) {
          sweepList.value.push(delList.value[0])
        }
        delList.value = sweepList.value.splice(i, 1)
        emits('update:list', sweepList.value)
        throw new Error()
      }

      if (params.resetPoint) {
        item.forEach((subItem: Point) => {
          drawCircle(subItem.x, subItem.y, 'rgb(0,180,226)')
        })
      }
    })

    delList.value.forEach((item) => {
      drawPic(item, 'rgba(233,79,79, 0.5)')
      if (
        params.point.x != 0 &&
        params.point.y != 0 &&
        canvasObj.value.isPointInPath(params.point.x, params.point.y)
      ) {
        let temp = { ...item }
        sweepList.value.push(temp)
        delList.value = []
        emits('update:list', sweepList.value)
        throw new Error()
      }

      if (params.resetPoint) {
        item.forEach((subItem: Point) => {
          drawCircle(subItem.x, subItem.y, 'rgb(233,79,79)')
        })
      }
    })
  } catch (e) {
    drawList()
  }
}

// 放大缩小重置
function drawAreaSet(type: string) {
  let imgWrapRef = imgWrap.value
  let left = imgWrapRef.style.left.slice(0, -2) / currentSize.value
  let top = imgWrapRef.style.top.slice(0, -2) / currentSize.value
  if (['1', '2'].includes(type)) {
    if (type == '1') {
      if (currentSize.value == 5) {
        return
      }
      currentSize.value += 0.5
    } else if (type == '2') {
      if (currentSize.value == 1) {
        return
      }
      currentSize.value -= 0.5
    }
    imgWrapRef.style.transformOrigin = `0% 0%`
  } else {
    currentSize.value = 1
  }

  imgWrapRef.style.transform = `scale(${currentSize.value})`
  if (type == '3') {
    reset()
  } else {
    reset(left, top)
  }
}

// 复位居中
function reset(left: number = 1, top: number = 1) {
  let imgWrapRef = imgWrap.value
  let imgWidth = imgWrapRef.offsetWidth
  let imgHeight = imgWrapRef.offsetHeight
  let canvasWrapWidth = canvasWrap.value.offsetWidth
  let canvasWrapHeight = canvasWrap.value.offsetHeight

  if (left == 1 && top == 1) {
    // 居中
    imgWrapRef.style.left = Math.ceil((canvasWrapWidth - imgWidth) / 2) + 'px'
    imgWrapRef.style.top = Math.ceil((canvasWrapHeight - imgHeight) / 2) + 'px'
  } else {
    // 基于当前位置放大缩小
    imgWrapRef.style.left = (left as number) * currentSize.value + 'px'
    imgWrapRef.style.top = (top as number) * currentSize.value + 'px'
  }
}

// 删除选择的绘制图形
function delArea() {
  delList.value = []
  drawList()
}

// 重置画板
function init() {
  sweepList.value = []
  delList.value = []
  clear()
}

defineExpose({
  init
})
</script>

<style scoped lang="scss">
.area-conatiner {
  padding: 20px;
  .canvas-wrap {
    touch-action: none;
    position: relative;
    width: 900px;
    height: 455px;
    overflow: hidden;
    background-color: #e6ecef;
    .modal-img-wrap {
      touch-action: none;
      position: relative;
      left: 0;
      top: 0;
      .modal-img {
        position: absolute;
        touch-action: none;
        top: 0;
        left: 0;
      }
      .canvas {
        z-index: 2;
        position: absolute;
        touch-action: none;

        top: 0;
        left: 0;
        cursor: move;
      }
    }
    .radio {
      position: absolute;
      bottom: 14px;
      left: 14px;
      display: flex;
      flex-direction: column;
      z-index: 3;
      label {
        margin-top: 12px;
      }
    }
    .action-btn {
      position: absolute;
      z-index: 3;
      left: 10px;
      top: 10px;
      padding: 0 4px;
      .action-item {
        display: flex;
        align-items: center;
        margin-top: 6px;
        padding-bottom: 6px;
        cursor: pointer;
        img {
          height: 40px;
        }
      }
      &.map-set {
        top: auto;
        left: auto;
        right: 10px;
        bottom: 10px;
      }
    }
  }
}
</style>
interface Point {
  x: number
  y: number
}

/**
 * 获取动态图片地址
 * @param {url} string
 * @returns {string}
 */
export const getImage = (url: string) => {
  let path: string = `../assets/images/${url}.png`
  const modules: any = import.meta.globEager('../assets/images/**/**.png')
  return modules[path].default
}

/**
 * 检查图形有没有横穿
 * @param point
 * @param pointList
 * @returns
 */
export function checkPointCross(point: Point, pointList: Array<Point>) {
  if (pointList.length < 3) {
    return true
  }
  for (let i = 0; i < pointList.length - 2; ++i) {
    const re = isPointCross(pointList[i], pointList[i + 1], pointList[pointList.length - 1], point)
    if (re) {
      return false
    }
  }
  return true
}

/**
 * 检查是否是凹图形
 * @param point
 * @param pointList
 * @param isEnd
 * @returns
 */
export function checkPointConcave(point: Point, pointList: Array<Point>, isEnd: boolean) {
  if (pointList.length < 3) {
    return true
  }
  if (
    isPointConcave(
      pointList[pointList.length - 3],
      pointList[pointList.length - 2],
      pointList[pointList.length - 1],
      point
    )
  )
    return false

  // 如果是闭合时,point为起始点,需要再判断最后两条线与第一条线是否形成凹图形
  if (isEnd) {
    if (
      isPointConcave(
        pointList[pointList.length - 2],
        pointList[pointList.length - 1],
        pointList[0],
        pointList[1]
      )
    )
      return false
    if (isPointConcave(pointList[pointList.length - 1], pointList[0], pointList[1], pointList[2]))
      return false
  }
  return true
}

/**
 * 检查点有没有与当前点位置太近,如果太近就不认为是一个点
 * @param point
 * @param pointList
 * @param minPointNum
 * @returns
 */
export function checkPointClose(point: Point, pointList: Array<Point>, minPointNum: number) {
  for (let i = 0; i < pointList.length; ++i) {
    const distance = Math.sqrt(
      Math.abs(pointList[i].x - point.x) + Math.abs(pointList[i].y - point.y)
    )
    if (distance > 6) {
      continue
    }
    // 如果是在第一个点附近点的,那就认为是在尝试闭合图形
    if (pointList.length >= minPointNum && i === 0) {
      return 'closeFirst'
    }
    return false
  }
  return true
}

/**
 * 辅助函数 检查两个线是否交叉
 * @param line1P1
 * @param line1P2
 * @param line2P1
 * @param line2P2
 * @returns
 */
export function isPointCross(line1P1: Point, line1P2: Point, line2P1: Point, line2P2: Point) {
  const euqal =
    isEuqalPoint(line1P1, line2P1) ||
    isEuqalPoint(line1P1, line2P2) ||
    isEuqalPoint(line1P2, line2P1) ||
    isEuqalPoint(line1P2, line2P2)
  const re1 = isDirection(line1P1, line1P2, line2P1)
  const re2 = isDirection(line1P1, line1P2, line2P2)
  const re3 = isDirection(line2P1, line2P2, line1P1)
  const re4 = isDirection(line2P1, line2P2, line1P2)
  const re11 = re1 * re2
  const re22 = re3 * re4
  if (re11 < 0 && re22 < 0) return true
  if (euqal) {
    if (re1 === 0 && re2 === 0 && re3 === 0 && re4 === 0) return true
  } else {
    if (re11 * re22 === 0) return true
  }
  return false
}

/**
 * 辅助函数 检查三个线是否凹凸
 * @param point1
 * @param point2
 * @param point3
 * @param point4
 * @returns
 */
export function isPointConcave(point1: Point, point2: Point, point3: Point, point4: Point) {
  const re1 = isDirection(point1, point2, point3)
  const re2 = isDirection(point2, point3, point4)
  if (re1 * re2 <= 0) return true
  return false
}

/**
 * 辅助函数 判断两个点是否是同一个
 * @param point1
 * @param point2
 * @returns
 */
export function isEuqalPoint(point1: Point, point2: Point) {
  if (point1.x == point2.x && point1.y == point2.y) {
    return true
  }
}

/**
 * 辅助函数 检查第二条线的方向在第一条线的左还是右
 * @param point1
 * @param point2
 * @param point3
 * @returns
 */
export function isDirection(point1: Point, point2: Point, point3: Point) {
  // 假设point1是原点
  const p1 = getPointLine(point1, point2)
  const p2 = getPointLine(point1, point3)
  return crossLine(p1, p2)
}

/**
 * 辅助函数 获取以point1作为原点的线
 * @param point1
 * @param point2
 * @returns
 */
export function getPointLine(point1: Point, point2: Point) {
  const p1 = {
    x: point2.x - point1.x,
    y: point2.y - point1.y
  }
  return p1
}

/**
 * 辅助函数 两线叉乘 两线的起点必须一致
 * @param point1
 * @param point2
 * @returns
 */
export function crossLine(point1: Point, point2: Point) {
  return point1.x * point2.y - point2.x * point1.y
}

以上就是vue3通过canvas实现图片圈选功能的详细内容,更多关于vue3 canvas图片圈选的资料请关注脚本之家其它相关文章!

相关文章

  • vue.js的简单自动求和计算实例

    vue.js的简单自动求和计算实例

    今天小编就为大家分享一篇vue.js的简单自动求和计算实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-11-11
  • 1分钟Vue实现右键菜单

    1分钟Vue实现右键菜单

    今天给大家分享的是,如何在最短的时候内实现右键菜单。高效实现需求,避免重复造轮子。感兴趣的可以了解一下
    2021-10-10
  • vue2中引用及使用 better-scroll的方法详解

    vue2中引用及使用 better-scroll的方法详解

    这篇文章主要介绍了vue2中引用better-scroll和使用 better-scroll的方法,使用时有三个要点及注意事项在文中给大家详细介绍 ,需要的朋友可以参考下
    2018-11-11
  • vue实现弹幕功能

    vue实现弹幕功能

    这篇文章主要介绍了vue实现弹幕功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-10-10
  • Vue导出Excel功能的全过程记录

    Vue导出Excel功能的全过程记录

    在开发后台管理系统的时候,很多地方都要用到导出excel 表格,比如将table中的数据导出到本地,这篇文章主要给大家介绍了关于Vue导出Excel功能的相关资料,需要的朋友可以参考下
    2021-07-07
  • vuejs实现ready函数加载完之后执行某个函数的方法

    vuejs实现ready函数加载完之后执行某个函数的方法

    这篇文章主要介绍了vuejs实现ready函数加载完之后执行某个函数的方法,需要的朋友可以参考下
    2018-08-08
  • Vue.js实现一个SPA登录页面的过程【推荐】

    Vue.js实现一个SPA登录页面的过程【推荐】

    本篇文章主要介绍了Vue.js写一个SPA登录页面过程的相关知识,具有很好的参考价值。下面跟着小编一起来看下吧
    2017-04-04
  • vue利用Moment插件格式化时间的实例代码

    vue利用Moment插件格式化时间的实例代码

    Moment.js 是一个 JavaScript 日期处理类库,用于解析、检验、操作、以及显示日期。这篇文章主要给大家介绍了关于vue利用Moment插件格式化时间的相关资料,需要的朋友可以参考下
    2021-05-05
  • Vue——前端生成二维码的示例

    Vue——前端生成二维码的示例

    这篇文章主要介绍了Vue——前端生成二维码的示例,帮助大家更好的理解和使用vue框架,感兴趣的朋友可以了解下
    2020-12-12
  • 使用vue中的v-for遍历二维数组的方法

    使用vue中的v-for遍历二维数组的方法

    下面小编就为大家分享一篇使用vue中的v-for遍历二维数组的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-03-03

最新评论