canvas/pages/index/transform-canvas.vue
2025-08-13 18:14:56 +08:00

378 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 子组件 transform-canvas.vue -->
<template>
<view>
<canvas
id="myCanvas"
canvas-id="myCanvas"
type="2d"
:style="{
width: `${canvasDisplayWidth}px`,
height: `${canvasDisplayHeight}px`,
border: '1px solid #eee'
}"
></canvas>
</view>
</template>
<script>
export default {
props: {
width: Number, // 画布实际宽度(像素)
height: Number, // 画布实际高度(像素)
imgUrl: String, // 图片URL
matrix: { // 变换矩阵
type: Object,
default: () => ({
a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0
})
},
areaData:{},
seatData:{},
selectedCodes:{}
},
data() {
return {
canvasContext: null,
image: null,
dpr: 1,
canvasDisplayWidth: 800, // 默认显示尺寸
canvasDisplayHeight: 600,
canvasActualWidth: 800, // 实际像素尺寸
canvasActualHeight: 600,
nowSelectedCodes:this.selectedCodes
};
},
watch: {
selectedCodes:{
handler(newVal) {
this.nowSelectedCodes = newVal
},
immediate: true
},
imgUrl: {
handler(newUrl) {
if (newUrl) this.loadImage(newUrl);
},
immediate: true
},
width: {
handler() {
this.updateCanvasSize();
},
immediate: true
},
height: {
handler() {
this.updateCanvasSize();
},
immediate: true
},
matrix: {
deep: true,
immediate: true, // 添加立即触发
handler() {
this.$nextTick(this.redraw); // 确保在下一个tick重绘
}
},
areaData: {
deep: true,
handler() {
this.$nextTick(this.redraw);
}
},
seatData: {
deep: true,
handler() {
this.$nextTick(this.redraw);
}
}
},
mounted() {
this.initCanvas();
},
methods: {
invertPoint(x, y) {
// 注意直接使用矩阵的逆转换不需要除以dpr
const inverted = this.matrix.invertPoint(x, y);
return inverted;
},
// 添加点击检测方法
checkHitArea(x, y) {
if (!this.areaData) return null;
// 转换坐标到原始画布空间
const inverted = this.matrix.invertPoint(x, y, this.dpr);
console.log('checkHitArea',inverted)
// 遍历所有区域检测
for (const area of this.areaData) {
if (this.pointInPolygon(inverted.x, inverted.y, area.polygon)) {
return area;
}
}
return null;
},
pointInPolygon(x, y, polygon) {
const points = [];
for (let i = 0; i < polygon.length; i += 2) {
points.push([polygon[i], polygon[i + 1]]);
}
let inside = false;
let j = points.length - 1;
for (let i = 0; i < points.length; j = i++) {
const [xi, yi] = points[i];
const [xj, yj] = points[j];
// 检测点是否在线段上
if (this.pointOnLineSegment(x, y, xi, yi, xj, yj)) return true;
// 射线法核心逻辑
if (yi <= y && yj > y || yj <= y && yi > y) {
const slope = (xj - xi) / (yj - yi);
if (x <= xi + (y - yi) * slope) {
inside = !inside;
}
}
}
return inside;
},
// 添加点是否在线段上的检查
pointOnLineSegment(x, y, x1, y1, x2, y2) {
// 线段的长度
const dx = x2 - x1;
const dy = y2 - y1;
const segmentLength = Math.sqrt(dx * dx + dy * dy);
// 点到线段起点的向量
const vx = x - x1;
const vy = y - y1;
// 向量在方向上的投影
const dotProduct = vx * dx + vy * dy;
// 计算参数t (0-1之间表示在线段上)
const t = dotProduct / (segmentLength * segmentLength);
// 点在线段上
if (t >= 0 && t <= 1) {
// 计算点在线段上的位置
const projX = x1 + t * dx;
const projY = y1 + t * dy;
// 检查点与投影点的距离容差5px
const distance = Math.sqrt((x - projX) * 2 + (y - projY) * 2);
return distance < 5;
}
return false;
},
initCanvas() {
const query = uni.createSelectorQuery().in(this);
query.select('#myCanvas')
.fields({ node: true, size: true })
.exec(async (res) => {
if (!res[0]) return;
this.canvas = res[0].node;
this.ctx = res[0].node.getContext('2d');
// 获取设备像素比
this.dpr = uni.getSystemInfoSync().pixelRatio;
// 更新画布尺寸
this.updateCanvasSize();
// 如果已有图片URL加载图片
if (this.imgUrl) {
await this.loadImage(this.imgUrl);
}
});
},
updateCanvasSize() {
// 显示尺寸使用prop传入的width/height
this.canvasDisplayWidth = this.width;
this.canvasDisplayHeight = this.height;
// * this.dpr
// * this.dpr
// 实际像素尺寸考虑DPR
this.canvasActualWidth = this.width;
this.canvasActualHeight = this.height;
if (this.canvas) {
this.canvas.width = this.canvasActualWidth;
this.canvas.height = this.canvasActualHeight;
// this.ctx.scale(this.dpr, this.dpr);
this.redraw();
}
},
async loadImage(src) {
try {
// 等待canvas初始化
if (!this.canvas) {
await this.initCanvas();
}
this.image = await new Promise((resolve, reject) => {
if (!this.canvas) {
// reject(new Error("Canvas not initialized"));
return;
}
const image = this.canvas.createImage();
image.src = src;
image.onload = () => resolve(image);
image.onerror = reject;
});
this.redraw();
} catch (e) {
console.error("图片加载失败:", e);
this.image = null;
}
},
// 在redraw方法中添加视图判断
checkSeatHit(x, y) {
if (!this.seatData) return null;
// 转换坐标到原始画布空间
const inverted = this.matrix.invertPoint(x, y, this.dpr);
// 遍历所有座位检测
for (const seat of this.seatData) {
const dx = seat.x - inverted.x;
const dy = seat.y - inverted.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 命中检测8px半径
if (distance < 8) {
return seat;
}
}
return null;
},
redraw() {
if (!this.ctx) return;
this.ctx.save();
this.ctx.resetTransform();
this.ctx.clearRect(0, 0, this.canvasActualWidth, this.canvasActualHeight);
this.ctx.scale(this.dpr, this.dpr);
// 应用变换矩阵
const { a, b, c, d, tx, ty } = this.matrix;
this.ctx.setTransform(a, b, c, d, tx, ty);
// 仅在区域视图显示背景图片
if (this.image && this.$parent?.currentView === 'area') {
this.ctx.drawImage(
this.image,
0,
0,
this.canvasDisplayWidth,
this.canvasDisplayHeight
);
}
// 绘制区域(仅在区域视图)
if (this.areaData && this.areaData.length > 0 && this.$parent?.currentView === 'area') {
this.drawAreas();
}
// 绘制座位(仅在座位视图)
if (this.seatData && this.seatData.length > 0 && this.$parent?.currentView === 'seat') {
this.drawSeats();
}
this.ctx.restore();
},
// 添加绘制区域方法
drawAreas() {
this.areaData.forEach(area => {
const points = [];
for (let i = 0; i < area.polygon.length; i += 2) {
points.push([area.polygon[i], area.polygon[i + 1]]);
}
this.ctx.beginPath();
points.forEach(([x, y], index) => {
if (index === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
});
this.ctx.closePath();
this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.7)';
this.ctx.lineWidth = 2;
this.ctx.stroke();
this.ctx.fillStyle = 'rgba(0, 255, 0, 0.2)';
this.ctx.fill();
});
},
drawSeats() {
this.ctx.save();
// 应用变换矩阵
const { a, b, c, d, tx, ty } = this.matrix;
this.ctx.setTransform(a, b, c, d, tx, ty);
// 获取当前缩放比例
const scale = Math.sqrt(a * a + b * b);
this.seatData.forEach(seat => {
const x = seat.x;
const y = seat.y;
// 根据缩放调整座位大小
const radius = 8 / scale;
console.log(this.nowSelectedCodes,'nowSelectedCodesnowSelectedCodesnowSelectedCodes')
// 修改1为选中座位添加特殊样式
if (this.nowSelectedCodes.has(seat.code) && seat.status === 1) {
// 1. 绘制金色边框
this.ctx.beginPath();
this.ctx.arc(x, y, radius + 1, 0, Math.PI * 2);
this.ctx.strokeStyle = '#FFD700'; // 金色
this.ctx.lineWidth = 2 / scale;
this.ctx.stroke();
// 2. 绘制半透明金色背景
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2);
this.ctx.fillStyle = 'rgba(255, 215, 0, 0.3)';
this.ctx.fill();
// 3. 绘制白色对勾(缩小版)
this.ctx.strokeStyle = '#fff';
this.ctx.lineWidth = 1 / scale;
this.ctx.lineCap = 'round';
this.ctx.beginPath();
this.ctx.moveTo(x - 2.5, y);
this.ctx.lineTo(x - 0.5, y + 1.5);
this.ctx.lineTo(x + 2.5, y - 2);
this.ctx.stroke();
} else {
// 普通座位的绘制保持不变
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2);
this.ctx.fillStyle = seat.status === 1 ? '#4cd964' : '#dd524d';
this.ctx.fill();
}
// 仅在缩放足够大时显示文字
});
this.ctx.restore();
}
}
}
</script>