vue3通過canvas實(shí)現(xiàn)圖片圈選功能
canvas實(shí)現(xiàn)圈選
具體效果
思路
- 容器里包裹著一張圖片和一個canvas, 讓其同等大小,在圖片加載完成后獲取到圖片大小再設(shè)置canvas大小。
- 要能拖動, 需要設(shè)置定位,要實(shí)現(xiàn)繪制,所以canvas要置于圖片上層,通過z-index設(shè)置,兩種功能不能同時實(shí)現(xiàn),需要通過按鈕開啟。
- 實(shí)現(xiàn)交點(diǎn)處按鈕拖拽重繪,此處的點(diǎn)不能使用canvas繪制,canvas繪制不具備DOM元素?zé)o法添加事件,此處可以通過DOM來繪制交點(diǎn)實(shí)心圓。為實(shí)心圓添加移動等等事件,拖動重繪,此處要注意,拖動重繪的時候不要重繪交點(diǎn),不然會拖動一次后移動事件就會失效。
- 選中刪除, 通過canvas的isPointInPath方法來進(jìn)行判斷,若是選中點(diǎn)存在繪制圖形擇重繪。
- 通過監(jiān)聽touchStart是否存在兩個觸摸點(diǎn)來實(shí)現(xiàn)圖片的手勢放大縮小。
思路1
頁面加載完之后,設(shè)置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
根據(jù)標(biāo)識判斷是繪制還是拖動圖片, 拖動的情況下判斷是不是點(diǎn)擊了交點(diǎn),如果是交點(diǎn)就拖動交點(diǎn)重繪,如果不是交點(diǎn)就拖動圖片。如果是繪制則每次點(diǎn)的時候都繪制一個實(shí)心圓并添加相應(yīng)拖動事件,繪制情況下到達(dá)設(shè)置點(diǎn)個數(shù)或者交點(diǎn)位置相近則自動閉合圖形。
// 繪制圓點(diǎn) 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
每次點(diǎn)擊的時候記錄點(diǎn)下的坐標(biāo)點(diǎn),當(dāng)是拖動模式下并且點(diǎn)下與彈起是的坐標(biāo)點(diǎn)相同,則認(rèn)為是選繪制圖形操作,判斷這個坐標(biāo)點(diǎn)是否存在于canvas繪制的圖形上,存在則選中重繪。
// 記錄當(dāng)前點(diǎn)擊坐標(biāo) let pointDown = { x: e.offsetX, y: e.offsetY } if (!openDraw.value) { curPoint.value = pointDown } // 記錄當(dāng)前點(diǎn)擊坐標(biāo), 用于判斷是否為選中區(qū)域, 用于處理選中刪除 if (!openDraw.value) { if (e.offsetX == curPoint.value.x && e.offsetY == curPoint.value.y) { drawList({ point: { x: e.offsetX, y: e.offsetY }, resetPoint: true }) } } // 判斷傳入坐標(biāo)是否在canvas上 canvasObj.value.isPointInPath(params.point.x, params.point.y)
思路5
雙指放大和縮小, 記錄第一次按下兩點(diǎn)間的距離,監(jiān)聽移動事件,記錄新的距離,計算兩個距離之間的倍數(shù)關(guān)系, 通過當(dāng)前倍數(shù)做限制,最后通過scale實(shí)現(xiàn)圖片的放大縮小。
// 雙指放大縮小 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}`) }) //區(qū)域選擇 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) } // 繪制圓點(diǎn) 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 } // 記錄當(dāng)前點(diǎn)擊坐標(biāo) 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) // 已經(jīng)有點(diǎn)了,連成線 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 }) // 如果已經(jīng)到達(dá)最大數(shù)量,則直接閉合圖形 if (pointList.value.length >= maxPointNum.value) { closeFigure() return } e.preventDefault() } else { //圖片拖拽 e.preventDefault() // 添加指針移動事件 imgWrapRef.addEventListener('pointermove', move) // 添加指針抬起事件,鼠標(biāo)抬起,將事件移除 imgWrapRef.addEventListener('pointerup', () => { imgWrapRef.removeEventListener('pointermove', move) }) // 指針離開父級元素,把事件移除 imgWrapRef.addEventListener('pointerleave', () => { imgWrapRef.removeEventListener('pointermove', move) }) } } function mouseup(e: any) { // 記錄當(dāng)前點(diǎn)擊坐標(biāo), 用于判斷是否為選中區(qū)域, 用于處理選中刪除 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('閉合圖形時發(fā)生橫穿線,請重新繪制!') clear() return } if (!checkPointConcave(pointList.value[0], pointList.value, true)) { ElMessage.error('閉合圖形時出現(xiàn)凹多邊形,請重新繪制!') 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個點(diǎn)!') } } 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個區(qū)域') 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() } //重新繪制成功的區(qū)域圖 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) } } // 復(fù)位居中 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 { // 基于當(dāng)前位置放大縮小 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 } /** * 獲取動態(tài)圖片地址 * @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為起始點(diǎn),需要再判斷最后兩條線與第一條線是否形成凹圖形 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 } /** * 檢查點(diǎn)有沒有與當(dāng)前點(diǎn)位置太近,如果太近就不認(rèn)為是一個點(diǎn) * @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 } // 如果是在第一個點(diǎn)附近點(diǎn)的,那就認(rèn)為是在嘗試閉合圖形 if (pointList.length >= minPointNum && i === 0) { return 'closeFirst' } return false } return true } /** * 輔助函數(shù) 檢查兩個線是否交叉 * @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 } /** * 輔助函數(shù) 檢查三個線是否凹凸 * @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 } /** * 輔助函數(shù) 判斷兩個點(diǎn)是否是同一個 * @param point1 * @param point2 * @returns */ export function isEuqalPoint(point1: Point, point2: Point) { if (point1.x == point2.x && point1.y == point2.y) { return true } } /** * 輔助函數(shù) 檢查第二條線的方向在第一條線的左還是右 * @param point1 * @param point2 * @param point3 * @returns */ export function isDirection(point1: Point, point2: Point, point3: Point) { // 假設(shè)point1是原點(diǎn) const p1 = getPointLine(point1, point2) const p2 = getPointLine(point1, point3) return crossLine(p1, p2) } /** * 輔助函數(shù) 獲取以point1作為原點(diǎn)的線 * @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 } /** * 輔助函數(shù) 兩線叉乘 兩線的起點(diǎn)必須一致 * @param point1 * @param point2 * @returns */ export function crossLine(point1: Point, point2: Point) { return point1.x * point2.y - point2.x * point1.y }
以上就是vue3通過canvas實(shí)現(xiàn)圖片圈選功能的詳細(xì)內(nèi)容,更多關(guān)于vue3 canvas圖片圈選的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue中ElementUI分頁組件Pagination的使用方法
這篇文章主要為大家詳細(xì)介紹了Vue中ElementUI分頁組件Pagination的使用,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-05-05Vue3實(shí)現(xiàn)vueFLow流程組件的詳細(xì)指南
VueFlow是一個專門為Vue.js框架設(shè)計的交互式可視化庫,它允許開發(fā)者輕松創(chuàng)建和管理復(fù)雜的圖形模型,如流程圖、狀態(tài)機(jī)、組織結(jié)構(gòu)圖等,本文給大家介紹了Vue3實(shí)現(xiàn)vueFLow流程組件的詳細(xì)指南,需要的朋友可以參考下2024-11-11關(guān)于Vue.nextTick()的正確使用方法淺析
最近在項(xiàng)目中遇到了一個需求,我們通過Vue.nextTick()來解決這一需求,但發(fā)現(xiàn)網(wǎng)上這方面的資料較少,所以自己來總結(jié)下,下面這篇文章主要給大家介紹了關(guān)于Vue.nextTick()正確使用方法的相關(guān)資料,需要的朋友可以參考下。2017-08-08VUE 文字轉(zhuǎn)語音播放的實(shí)現(xiàn)示例
本文主要介紹了VUE 文字轉(zhuǎn)語音播放的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02vue項(xiàng)目打包后部署到服務(wù)器的詳細(xì)步驟
這篇文章主要介紹了vue項(xiàng)目打包后部署到服務(wù)器,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-09-09