完整代码
关联文章-canvas做线段和多边形可随意拖动拉拽

<!-- canvas上实现元素的拖拽和拉伸 -->
<template>
<div ref="containerRef" class="common-layout">
<canvas ref="canvasRef" @mousedown="onDown" @mouseup="onUp" @mousemove="onMove" @mouseleave="onLeave"
:style="{ width: cWidth + 'px', height: cHeight + 'px' }" class="canvas_css"></canvas>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue'
import { enhancedThrottle, debounce } from '@/utils/performance'
let cWidth = 800
let cHeight = 800
const isDragging = ref();
const dragTarget = ref(null);
const isDraggingOverall = ref(false);
let dragRectDots = reactive([]);
let fixedPoint = reactive({});
const containerRef = ref(null);
const canvasRef = ref(null)
let canvas = null
let rect = null
let ctx = null
let offsetX = ref(0), offsetY = ref(0);
const radius = 5;
const elements = reactive([
{
type: 'line',
name: '线条一',
color: '#fe39aa',
dotList: [
{
x: 10,
y: 10,
radius: radius,
color: 'red'
},
{
x: 50,
y: 50,
radius: radius,
color: 'blue'
}
]
},
{
type: 'polygon',
name: '多边形一',
color: '#fe39aa',
fillColor: '#84a729',
dotList: [
{ x: 330, y: 140, radius: radius, color: 'red' },
{ x: 250, y: 140, radius: radius, color: 'blue' },
{ x: 100, y: 100, radius: radius, color: 'green' },
{ x: 300, y: 80, radius: radius, color: '#00ffff' },
{ x: 420, y: 100, radius: radius, color: '#ffff00' },
]
},
{
type: 'rect',
name: '矩形一',
color: '#fe39aa',
fillColor: '#e60012',
dotList: [
{ x: 330, y: 140, radius: radius, color: 'red' },
{ x: 450, y: 140, radius: radius, color: 'blue' },
{ x: 450, y: 200, radius: radius, color: 'green' },
{ x: 330, y: 200, radius: radius, color: '#ffff00' },
]
},
{
type: 'square',
name: '正方形一',
color: '#fe39aa',
fillColor: '#2e59a7',
dotList: [
{ x: 200, y: 300, radius: radius, color: 'red' },
{ x: 300, y: 300, radius: radius, color: 'blue' },
{ x: 300, y: 400, radius: radius, color: 'green' },
{ x: 200, y: 400, radius: radius, color: '#ffff00' },
]
},
])
const onDown = (e) => {
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
console.log('按下', mouseX, mouseY)
for (let i = elements.length - 1; i >= 0; i--) {
const el = elements[i];
if (el.type === 'line') {
const onCircle = setMoveDotInfo('line', el, i, mouseX, mouseY)
if (onCircle) return
if (isHitLine(mouseX, mouseY, el)) {
console.log('onDown在线上')
setMoveOverallInfo('line', el, i, mouseX, mouseY)
return;
}
} else if (el.type === 'polygon') {
const onCircle = setMoveDotInfo('polygon', el, i, mouseX, mouseY)
if (onCircle) return
if (isHitPolygon(mouseX, mouseY, el)) {
console.log('onDown在多边形内')
setMoveOverallInfo('polygon', el, i, mouseX, mouseY)
return;
}
} else if (el.type === 'rect') {
const onCircle = setMoveDotInfo('rect', el, i, mouseX, mouseY)
if (onCircle) return
if (isHitPolygon(mouseX, mouseY, el)) {
console.log('onDown在矩形内')
setMoveOverallInfo('rect', el, i, mouseX, mouseY)
return;
}
} else if (el.type === 'square') {
const onCircle = setMoveDotInfo('square', el, i, mouseX, mouseY)
if (onCircle) return
if (isHitPolygon(mouseX, mouseY, el)) {
console.log('onDown在正方形内')
setMoveOverallInfo('square', el, i, mouseX, mouseY)
return;
}
}
}
}
const setMoveDotInfo = (type, el, i, mouseX, mouseY) => {
const { dotList } = el;
for (let j = 0; j < dotList.length; j++) {
const forCirculation = {
j, i, dotList
}
if (isHitCircle(mouseX, mouseY, dotList[j])) {
console.log('onDown在端点内')
setControlOptions(el, false, type, forCirculation, mouseX, mouseY);
return true;
}
}
}
const setControlOptions = (el, draggingOverall, type, forCirculation, mouseX, mouseY) => {
const { j, i, dotList } = forCirculation
const elDot = dotList[j];
dragTarget.value = elDot;
isDraggingOverall.value = draggingOverall;
isDragging.value = type
offsetX.value = mouseX - dotList[j].x;
offsetY.value = mouseY - dotList[j].y;
if (type === 'rect') {
dragRectDots = dotList.filter(dot => {
if (dot.x !== elDot.x && dot.y !== elDot.y) {
fixedPoint = dot
}
return (dot.x === elDot.x || dot.y === elDot.y) && !(dot.x === elDot.x && dot.y === elDot.y)
})
} else if (type === 'square') {
dragRectDots = [dotList[(j + 1) % 4], dotList[(j + 3) % 4]]
fixedPoint = dotList[(j + 2) % 4]
}
const a = dotList.splice(j + 1);
dotList.unshift(...a)
elements.splice(i, 1);
elements.push(el);
render();
}
const setMoveOverallInfo = (type, el, i, mouseX, mouseY) => {
dragTarget.value = el;
offsetX.value = mouseX;
offsetY.value = mouseY;
isDraggingOverall.value = true;
isDragging.value = type
elements.splice(i, 1);
elements.push(el);
render();
}
const onUp = (e) => {
isDragging.value = ''
dragTarget.value = null;
console.log('elements', elements)
}
const onLeave = (e) => {
if (!isDragging.value) return
if (!dragTarget.value) return;
setNewPos(e);
render();
isDragging.value = ''
dragTarget.value = null;
}
const onMove = enhancedThrottle((e) => {
if (!isDragging.value) return
if (!dragTarget.value) return;
setNewPos(e);
render();
}, 50)
const setNewPos = (e) => {
if (!isDraggingOverall.value) {
const x = e.clientX - rect.left - offsetX.value < 0 ? 0 : e.clientX - rect.left - offsetX.value > canvas.width ? canvas.width : e.clientX - rect.left - offsetX.value;
const y = e.clientY - rect.top - offsetY.value < 0 ? 0 : e.clientY - rect.top - offsetY.value > canvas.height ? canvas.height : e.clientY - rect.top - offsetY.value;
dragTarget.value.x = x;
dragTarget.value.y = y;
if (isDragging.value === 'rect') {
dragRectDots.forEach(dot => {
let dx = x, dy = y;
if (dot.x === fixedPoint.x) {
dx = fixedPoint.x
} else if (dot.y === fixedPoint.y) {
dy = fixedPoint.y
}
dot.x = dx
dot.y = dy
})
} else if (isDragging.value === 'square') {
updateAdjacentPoints(x, y)
}
} else {
console.log(dragTarget.value, offsetY.value)
console.log(e.clientY, rect.top)
const dx = e.clientX - rect.left - offsetX.value;
const dy = e.clientY - rect.top - offsetY.value;
offsetX.value = e.clientX - rect.left;
offsetY.value = e.clientY - rect.top;
console.log('xxxxx', offsetY.value)
dragTarget.value.dotList.forEach(el => {
el.x += dx;
el.y += dy;
});
}
}
const updateAdjacentPoints = (newX, newY) => {
const center = {
x: (newX + fixedPoint.x) / 2,
y: (newY + fixedPoint.y) / 2
};
const vectorToDragged = {
x: newX - center.x,
y: newY - center.y
};
const vectorToAdjacent1 = rotateVector90(vectorToDragged);
const vectorToAdjacent2 = rotateVectorNeg90(vectorToDragged);
dragRectDots.forEach((dot, index) => {
if (index === 0) {
dot.x = center.x + vectorToAdjacent1.x
dot.y = center.y + vectorToAdjacent1.y
} else if (index === 1) {
dot.x = center.x + vectorToAdjacent2.x
dot.y = center.y + vectorToAdjacent2.y
}
})
console.log('更新相邻点1', dragRectDots)
console.log('更新相邻点2', elements.filter(i => i.type === 'square'))
function rotateVector90(vector) {
return { x: -vector.y, y: vector.x };
}
function rotateVectorNeg90(vector) {
return { x: vector.y, y: -vector.x };
}
}
const isHitLine = (x, y, element, range = 12) => {
const [start, end] = element.dotList;
const dx = end.x - start.x;
const dy = end.y - start.y;
const lineLength = Math.sqrt(dx * dx + dy * dy);
if (lineLength === 0) {
return Math.sqrt((x - start.x) ** 2 + (y - start.y) ** 2) <= range;
}
const t = ((x - start.x) * dx + (y - start.y) * dy) / (lineLength * lineLength);
if (t < 0 || t > 1) {
return false;
}
const px = start.x + t * dx;
const py = start.y + t * dy;
const distanceToProjection = Math.sqrt((x - px) ** 2 + (y - py) ** 2);
return distanceToProjection <= range;
}
const isHitCircle = (x, y, element) => {
const dx = x - element.x;
const dy = y - element.y;
return (dx * dx + dy * dy) <= Math.pow(element.radius, 2);
}
const isHitPolygon = (x, y, element) => {
const points = element.dotList;
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
if (((points[i].y <= y && y < points[j].y) || (points[j].y <= y && y < points[i].y)) &&
(x < (points[j].x - points[i].x) * (y - points[i].y) / (points[j].y - points[i].y) + points[i].x)) {
inside = !inside;
}
}
return inside;
}
const drawPolygon = (polygonInfo) => {
ctx.beginPath()
const LineColor = polygonInfo.color
polygonInfo.dotList.forEach(({ x, y, color }, index) => {
if (index === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
})
ctx.strokeStyle = LineColor
ctx.closePath();
ctx.fillStyle = polygonInfo.fillColor || LineColor
ctx.fill()
ctx.strokeStyle = polygonInfo.color || LineColor
ctx.stroke()
const midX = polygonInfo.dotList.reduce((acc, { x }) => acc + x, 0) / polygonInfo.dotList.length
const midY = polygonInfo.dotList.reduce((acc, { y }) => acc + y, 0) / polygonInfo.dotList.length
drawText(polygonInfo.name, midX, midY)
polygonInfo.dotList.forEach(({ x, y, radius, color }, index) => {
drawCircle(x, y, radius, color)
})
}
const drawLine = (lineInfo) => {
const [start, end] = lineInfo.dotList
const color = lineInfo.color
ctx.beginPath()
ctx.moveTo(start.x, start.y)
ctx.lineTo(end.x, end.y)
ctx.lineWidth = 2;
ctx.strokeStyle = color
ctx.stroke()
const midX = (start.x + end.x) / 2
const midY = (start.y + end.y) / 2
drawText(lineInfo.name, midX, midY)
drawCircle(start.x, start.y, start.radius, (start.color || color))
drawCircle(end.x, end.y, end.radius, (end.color || color))
}
const drawCircle = (x, y, r, color) => {
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = color
ctx.fill();
}
const drawText = (text, x, y, option) => {
const defaultOption = { color: '#fff', size: 12, center: true }
option = Object.assign(defaultOption, option)
ctx.fillStyle = option.color
ctx.font = `${option.size}px Arial`
if (option.center) {
ctx.textAlign = 'center'
}
ctx.fillText(text, x, y)
}
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
elements.forEach(el => {
switch (el.type) {
case 'circle':
drawCircle(el.x, el.y, el.radius, el.color)
break;
case 'line':
drawLine(el)
break;
case 'polygon':
drawPolygon(el)
break;
case 'rect':
drawPolygon(el)
break;
case 'square':
drawPolygon(el)
break;
default:
console.error(`未知的元素类型:${el.type}`)
}
});
}
const initCanvas = () => {
const canvas = canvasRef.value
canvas.width = '800';
canvas.height = '800';
ctx = canvasRef.value.getContext('2d')
render()
}
onMounted(() => {
canvas = canvasRef.value
rect = canvas.getBoundingClientRect();
initCanvas()
})
</script>
<style scoped>
.common-layout {
background: gray;
}
.canvas_css {
background: #110909;
margin: 20px;
}
</style>