325 lines
8.8 KiB
Vue
325 lines
8.8 KiB
Vue
<!-- 子组件 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:{}
|
||
},
|
||
data() {
|
||
return {
|
||
canvasContext: null,
|
||
image: null,
|
||
dpr: 1,
|
||
canvasDisplayWidth: 800, // 默认显示尺寸
|
||
canvasDisplayHeight: 600,
|
||
canvasActualWidth: 800, // 实际像素尺寸
|
||
canvasActualHeight: 600
|
||
};
|
||
},
|
||
watch: {
|
||
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重绘
|
||
}
|
||
},
|
||
},
|
||
mounted() {
|
||
this.initCanvas();
|
||
},
|
||
methods: {
|
||
// 添加点击检测方法
|
||
checkHitArea(x, y) {
|
||
if (!this.areaData) return null;
|
||
|
||
// 转换坐标到原始画布空间
|
||
const inverted = this.matrix.invertPoint(x, y);
|
||
console.log('checkHitArea',inverted)
|
||
// 遍历所有区域检测
|
||
for (const area of this.areaData) {
|
||
if (this.pointInPolygon(inverted.x, inverted.y, area.polygon)) {
|
||
return area.areacode;
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
// 射线法判断点是否在多边形内
|
||
// pointInPolygon(x, y, polygon) {
|
||
// // console.log('pointInPolygon',x, y, polygon)
|
||
// // 确保多边形有足够的点构成
|
||
// if (!polygon || polygon.length < 6) return false;
|
||
|
||
// let inside = false;
|
||
// const length = polygon.length;
|
||
|
||
// // 不需要遍历最后一个点,因为它是闭合点(与第一个点相同)
|
||
// for (let i = 0, j = length - 2; i < length; j = i, i += 2) {
|
||
// const xi = polygon[i], yi = polygon[i + 1];
|
||
// const xj = polygon[j], yj = polygon[j + 1];
|
||
|
||
// // 检查点是否在边的y值范围内
|
||
// const intersect = ((yi > y) !== (yj > y));
|
||
|
||
// // 检查点是否在边的左侧
|
||
// if (intersect) {
|
||
// const slope = (xj - xi) / (yj - yi); // 边的斜率
|
||
// const intersectX = xi + (y - yi) * slope;
|
||
|
||
// if (x < intersectX) {
|
||
// inside = !inside;
|
||
// }
|
||
// }
|
||
// }
|
||
// return inside;
|
||
// },
|
||
|
||
// 在 transform-canvas.vue 中替换 pointInPolygon 方法
|
||
pointInPolygon(x, y, polygon) {
|
||
// 确保多边形的点数是偶数且至少有3个顶点
|
||
if (!polygon || polygon.length < 6 || polygon.length % 2 !== 0) {
|
||
return false;
|
||
}
|
||
|
||
let inside = false;
|
||
// 转换多边形点数为顶点数
|
||
const vertexCount = polygon.length / 2;
|
||
let j = vertexCount - 1;
|
||
|
||
for (let i = 0; i < vertexCount; j = i++) {
|
||
// 获取当前顶点和上一个顶点
|
||
const xi = polygon[i * 2];
|
||
const yi = polygon[i * 2 + 1];
|
||
const xj = polygon[j * 2];
|
||
const yj = polygon[j * 2 + 1];
|
||
|
||
// 检查点是否在边界上 - 提高精度避免浮点数误差
|
||
const onLine = this.pointOnLineSegment(x, y, xi, yi, xj, yj);
|
||
if (onLine) return true;
|
||
|
||
// 检查点是否在两个顶点之间(射线法)
|
||
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
|
||
// 计算射线与边界的交点
|
||
const slope = (xj - xi) / (yj - yi);
|
||
const intersectX = xi + (y - yi) * slope;
|
||
|
||
// 如果交点在点右侧,标记为穿越
|
||
if (x < intersectX) {
|
||
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;
|
||
|
||
// 实际像素尺寸考虑DPR
|
||
this.canvasActualWidth = this.width * this.dpr;
|
||
this.canvasActualHeight = this.height * this.dpr;
|
||
|
||
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;
|
||
}
|
||
},
|
||
|
||
// async loadImage(src) {
|
||
// try {
|
||
// this.image = await new Promise((resolve, reject) => {
|
||
// 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;
|
||
// this.redraw(); // 加载失败时也需要重绘画布
|
||
// }
|
||
// },
|
||
|
||
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.ctx.drawImage(
|
||
this.image,
|
||
0,
|
||
0,
|
||
this.canvasDisplayWidth,
|
||
this.canvasDisplayHeight
|
||
);
|
||
}
|
||
// this.ctx.restore();
|
||
|
||
// 恢复上下文状态
|
||
if (this.areaData && this.areaData.length > 0) {
|
||
this.areaData.forEach(area => {
|
||
// if (!area.polygon || area.polygon.length < 6) return;
|
||
|
||
this.ctx.beginPath();
|
||
|
||
for (let i = 0; i < area.polygon.length; i += 2) {
|
||
// 使用标准API
|
||
if (i === 0) {
|
||
this.ctx.moveTo(area.polygon[i], area.polygon[i + 1]);
|
||
} else {
|
||
this.ctx.lineTo(area.polygon[i], area.polygon[i + 1]);
|
||
}
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
</script> |