canvas实现圈选
具体效果
思路
- 容器里包裹着一张图片和一个canvas, 让其同等大小,在图片加载完成后获取到图片大小再设置canvas大小。
- 要能拖动, 需要设置定位,要实现绘制,所以canvas要置于图片上层,通过z-index设置,两种功能不能同时实现,需要通过按钮开启。
- 实现交点处按钮拖拽重绘,此处的点不能使用canvas绘制,canvas绘制不具备DOM元素无法添加事件,此处可以通过DOM来绘制交点实心圆。为实心圆添加移动等等事件,拖动重绘,此处要注意,拖动重绘的时候不要重绘交点,不然会拖动一次后移动事件就会失效。
- 选中删除, 通过canvas的isPointInPath方法来进行判断,若是选中点存在绘制图形择重绘。
- 通过监听touchStart是否存在两个触摸点来实现图片的手势放大缩小。
思路1
页面加载完之后,设置canvas大小,如果存在圈选图则绘制,同时为容器添加touch事件用于双指缩小放大。
js
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
根据标识判断是绘制还是拖动图片, 拖动的情况下判断是不是点击了交点,如果是交点就拖动交点重绘,如果不是交点就拖动图片。如果是绘制则每次点的时候都绘制一个实心圆并添加相应拖动事件,绘制情况下到达设置点个数或者交点位置相近则自动闭合图形。
js
// 绘制圆点
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绘制的图形上,存在则选中重绘。
js
// 记录当前点击坐标
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实现图片的放大缩小。
js
// 双指放大缩小
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)
}
具体代码如下
html
<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>
ts
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
}