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图片圈选的资料请关注脚本之家其它相关文章!
最新评论