vue3通過(guò)canvas實(shí)現(xiàn)圖片圈選功能
canvas實(shí)現(xiàn)圈選
具體效果

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

