This commit is contained in:
sunmeng 2025-07-25 09:09:08 +08:00
parent 94e79c4b77
commit a99e07d5fb
7 changed files with 277 additions and 229 deletions

View File

@ -18,6 +18,7 @@
:width="canvasWidth" :width="canvasWidth"
:height="canvasHeight" :height="canvasHeight"
:matrix="transformMatrix" :matrix="transformMatrix"
:imgUrl="'https://assets.tech.troyrc.com/sx25/images/events/XBDT.jpg'"
/> />
</view> </view>
@ -220,7 +221,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
const canvas = this.$refs.canvasRef; const canvas = this.$refs.canvasRef;
if (canvas) { if (canvas) {
canvas.draw(); // canvas.draw();
// //
this.scaleValue = Math.sqrt( this.scaleValue = Math.sqrt(

View File

@ -1,154 +1,172 @@
<!-- 子组件 transform-canvas.vue -->
<template> <template>
<canvas <view>
canvas-id="gestureCanvas" <canvas
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" id="myCanvas"
/> canvas-id="myCanvas"
type="2d"
:style="{
width: `${canvasDisplayWidth}px`,
height: `${canvasDisplayHeight}px`,
border: '1px solid #eee'
}"
></canvas>
</view>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
width: Number, width: Number, // ()
height: Number, height: Number, // ()
matrix: Object imgUrl: String, // URL
matrix: { //
type: Object,
default: () => ({
a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0
})
}
}, },
data() { data() {
return { return {
ctx: null, canvasContext: null,
canvasWidth: this.width, image: null,
canvasHeight: this.height dpr: 1,
canvasDisplayWidth: 300, //
canvasDisplayHeight: 300,
canvasActualWidth: 300, //
canvasActualHeight: 300
}; };
}, },
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,
handler() {
this.$nextTick(() => {
if (this.canvasContext) {
this.redraw();
}
});
}
}
},
mounted() { mounted() {
this.initCanvas(); this.initCanvas();
}, },
methods: { methods: {
initCanvas() { initCanvas() {
this.ctx = uni.createCanvasContext('gestureCanvas', this); const query = uni.createSelectorQuery().in(this);
const pixelRatio = uni.getSystemInfoSync().pixelRatio; query.select('#myCanvas')
this.ctx.scale(pixelRatio, pixelRatio); .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);
}
});
}, },
applyTransform(matrix) { updateCanvasSize() {
if (!matrix) return; // 使propwidth/height
this.canvasDisplayWidth = this.width;
this.canvasDisplayHeight = this.height;
// // DPR
const pixelRatio = uni.getSystemInfoSync().pixelRatio; this.canvasActualWidth = this.width * this.dpr;
const tx = matrix.tx * pixelRatio; this.canvasActualHeight = this.height * this.dpr;
const ty = matrix.ty * pixelRatio;
// if (this.canvas) {
this.ctx.setTransform( this.canvas.width = this.canvasActualWidth;
matrix.a, this.canvas.height = this.canvasActualHeight;
matrix.b, this.ctx.scale(this.dpr, this.dpr);
matrix.c, this.redraw();
matrix.d, }
tx,
ty
);
}, },
// async loadImage(src) {
// drawDebugGrid() { try {
// if (!this.ctx) return; this.image = await new Promise((resolve, reject) => {
const image = this.canvas.createImage();
// const pixelRatio = uni.getSystemInfoSync().pixelRatio; image.src = src;
// const width = this.canvasWidth * pixelRatio; image.onload = () => resolve(image);
// const height = this.canvasHeight * pixelRatio; image.onerror = reject;
});
// // this.redraw();
// this.ctx.save(); } catch (e) {
console.error("图片加载失败:", e);
// // this.image = null;
// this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.redraw(); //
}
// // },
// this.ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
// this.ctx.lineWidth = 1;
// // 线
// for (let y = 0; y <= height; y += 50) {
// this.ctx.beginPath();
// this.ctx.moveTo(0, y);
// this.ctx.lineTo(width, y);
// this.ctx.stroke();
// }
// // 线
// for (let x = 0; x <= width; x += 50) {
// this.ctx.beginPath();
// this.ctx.moveTo(x, 0);
// this.ctx.lineTo(x, height);
// this.ctx.stroke();
// }
// //
// this.ctx.strokeStyle = '#ff0000';
// this.ctx.lineWidth = 2;
// this.ctx.beginPath();
// this.ctx.moveTo(0, 0);
// this.ctx.lineTo(width, 0);
// this.ctx.stroke();
// this.ctx.beginPath();
// this.ctx.moveTo(0, 0);
// this.ctx.lineTo(0, height);
// this.ctx.stroke();
// //
// this.ctx.restore();
// this.ctx.draw();
// },
// redraw() {
draw() { if (!this.ctx) return;
if (!this.ctx) {
console.warn('Canvas上下文未初始化'); //
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
);
} else {
//
this.ctx.fillStyle = '#f0f0f0';
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fillStyle = '#999';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('图片加载失败', this.width / 2, this.height / 2);
} }
// 1. Canvas //
this.clearCanvas(); this.ctx.restore();
// 2.
this.applyTransform(this.matrix);
// 3.
this.drawContent();
},
clearCanvas() {
const pixelRatio = uni.getSystemInfoSync().pixelRatio;
const width = this.canvasWidth * pixelRatio;
const height = this.canvasHeight * pixelRatio;
//
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, width, height);
},
drawContent() {
//
this.ctx.fillStyle = '#3498db';
this.ctx.fillRect(50, 50, 100, 100);
this.ctx.fillStyle = '#e74c3c';
this.ctx.beginPath();
this.ctx.arc(150, 150, 50, 0, 2 * Math.PI);
this.ctx.fill();
//
this.ctx.fillStyle = '#2c3e50';
this.ctx.font = '20px sans-serif';
this.ctx.fillText('手势Canvas', 50, 250);
//
this.ctx.draw();
} }
} }
}; }
</script> </script>

View File

@ -1 +1 @@
{"version":3,"file":"transform-canvas.js","sources":["/Users/sunmeng/Desktop/wx/canvas/pages/index/transform-canvas.vue?type=component"],"sourcesContent":["import Component from '/Users/sunmeng/Desktop/wx/canvas/pages/index/transform-canvas.vue'\nwx.createComponent(Component)"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,GAAG,gBAAgB,SAAS;"} {"version":3,"file":"transform-canvas.js","sources":["/Users/sunmeng/Desktop/wx/canvas/pages/index/transform-canvas.vue?type=component"],"sourcesContent":["import Component from '/Users/sunmeng/Desktop/wx/canvas/pages/index/transform-canvas.vue'\nwx.createComponent(Component)"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,GAAG,gBAAgB,SAAS;"}

View File

@ -6882,7 +6882,7 @@ function initOnError() {
function initRuntimeSocketService() { function initRuntimeSocketService() {
const hosts = "127.0.0.1,172.10.0.205"; const hosts = "127.0.0.1,172.10.0.205";
const port = "8090"; const port = "8090";
const id = "mp-weixin_aMdMwA"; const id = "mp-weixin_Z7-NJ2";
const lazy = typeof swan !== "undefined"; const lazy = typeof swan !== "undefined";
let restoreError = lazy ? () => { let restoreError = lazy ? () => {
} : initOnError(); } : initOnError();

View File

@ -35,7 +35,7 @@ const _sfc_main = {
container: this.containerRect container: this.containerRect
}); });
} catch (e) { } catch (e) {
common_vendor.index.__f__("error", "at pages/index/gesture-canvas-page.vue:76", "手势处理器初始化失败:", e); common_vendor.index.__f__("error", "at pages/index/gesture-canvas-page.vue:77", "手势处理器初始化失败:", e);
this.gestureHandler = { this.gestureHandler = {
catchEvent: (event) => { catchEvent: (event) => {
if (event.touches.length > 0) { if (event.touches.length > 0) {
@ -72,21 +72,21 @@ const _sfc_main = {
width: rect.width, width: rect.width,
height: rect.height height: rect.height
}; };
common_vendor.index.__f__("log", "at pages/index/gesture-canvas-page.vue:123", "成功获取容器位置:", this.containerRect); common_vendor.index.__f__("log", "at pages/index/gesture-canvas-page.vue:124", "成功获取容器位置:", this.containerRect);
return; return;
} }
} catch (e) { } catch (e) {
common_vendor.index.__f__("error", "at pages/index/gesture-canvas-page.vue:127", "获取容器位置失败:", e); common_vendor.index.__f__("error", "at pages/index/gesture-canvas-page.vue:128", "获取容器位置失败:", e);
} }
await new Promise((r) => setTimeout(r, 100)); await new Promise((r) => setTimeout(r, 100));
retryCount++; retryCount++;
} }
common_vendor.index.__f__("error", "at pages/index/gesture-canvas-page.vue:135", `容器位置获取失败,已重试${maxRetries}`); common_vendor.index.__f__("error", "at pages/index/gesture-canvas-page.vue:136", `容器位置获取失败,已重试${maxRetries}`);
}, },
// 事件处理 // 事件处理
async handleTouchEvent(event) { async handleTouchEvent(event) {
if (!this.gestureHandler || !this.containerRect) { if (!this.gestureHandler || !this.containerRect) {
common_vendor.index.__f__("warn", "at pages/index/gesture-canvas-page.vue:141", "手势处理器未就绪,尝试重新初始化..."); common_vendor.index.__f__("warn", "at pages/index/gesture-canvas-page.vue:142", "手势处理器未就绪,尝试重新初始化...");
await this.getContainerPosition(); await this.getContainerPosition();
this.initGestureHandler(); this.initGestureHandler();
if (event.type === "touchend" || event.type === "touchcancel") { if (event.type === "touchend" || event.type === "touchcancel") {
@ -113,7 +113,7 @@ const _sfc_main = {
const correctedTouches = Array.from(event.touches).map((touch) => { const correctedTouches = Array.from(event.touches).map((touch) => {
const x = touch.clientX - this.containerRect.left; const x = touch.clientX - this.containerRect.left;
const y = touch.clientY - this.containerRect.top; const y = touch.clientY - this.containerRect.top;
common_vendor.index.__f__("log", "at pages/index/gesture-canvas-page.vue:181", `原始坐标: (${touch.clientX}, ${touch.clientY}) 修正后: (${x}, ${y})`); common_vendor.index.__f__("log", "at pages/index/gesture-canvas-page.vue:182", `原始坐标: (${touch.clientX}, ${touch.clientY}) 修正后: (${x}, ${y})`);
return { return {
...touch, ...touch,
x, x,
@ -143,7 +143,6 @@ const _sfc_main = {
this.$nextTick(() => { this.$nextTick(() => {
const canvas = this.$refs.canvasRef; const canvas = this.$refs.canvasRef;
if (canvas) { if (canvas) {
canvas.draw();
this.scaleValue = Math.sqrt( this.scaleValue = Math.sqrt(
this.transformMatrix.a * this.transformMatrix.a + this.transformMatrix.b * this.transformMatrix.b this.transformMatrix.a * this.transformMatrix.a + this.transformMatrix.b * this.transformMatrix.b
); );
@ -184,7 +183,8 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
g: common_vendor.p({ g: common_vendor.p({
width: $data.canvasWidth, width: $data.canvasWidth,
height: $data.canvasHeight, height: $data.canvasHeight,
matrix: $data.transformMatrix matrix: $data.transformMatrix,
imgUrl: "https://assets.tech.troyrc.com/sx25/images/events/XBDT.jpg"
}), }),
h: common_vendor.o((...args) => $options.handleTouchEvent && $options.handleTouchEvent(...args)), h: common_vendor.o((...args) => $options.handleTouchEvent && $options.handleTouchEvent(...args)),
i: common_vendor.o((...args) => $options.handleTouchEvent && $options.handleTouchEvent(...args)), i: common_vendor.o((...args) => $options.handleTouchEvent && $options.handleTouchEvent(...args)),

View File

@ -3,117 +3,146 @@ const common_vendor = require("../../common/vendor.js");
const _sfc_main = { const _sfc_main = {
props: { props: {
width: Number, width: Number,
// 画布实际宽度(像素)
height: Number, height: Number,
matrix: Object // 画布实际高度(像素)
imgUrl: String,
// 图片URL
matrix: {
// 变换矩阵
type: Object,
default: () => ({
a: 1,
b: 0,
c: 0,
d: 1,
tx: 0,
ty: 0
})
}
}, },
data() { data() {
return { return {
ctx: null, canvasContext: null,
canvasWidth: this.width, image: null,
canvasHeight: this.height dpr: 1,
canvasDisplayWidth: 300,
// 默认显示尺寸
canvasDisplayHeight: 300,
canvasActualWidth: 300,
// 实际像素尺寸
canvasActualHeight: 300
}; };
}, },
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,
handler() {
this.$nextTick(() => {
if (this.canvasContext) {
this.redraw();
}
});
}
}
},
mounted() { mounted() {
this.initCanvas(); this.initCanvas();
}, },
methods: { methods: {
initCanvas() { initCanvas() {
this.ctx = common_vendor.index.createCanvasContext("gestureCanvas", this); const query = common_vendor.index.createSelectorQuery().in(this);
const pixelRatio = common_vendor.index.getSystemInfoSync().pixelRatio; query.select("#myCanvas").fields({ node: true, size: true }).exec(async (res) => {
this.ctx.scale(pixelRatio, pixelRatio); if (!res[0])
return;
this.canvas = res[0].node;
this.ctx = res[0].node.getContext("2d");
this.dpr = common_vendor.index.getSystemInfoSync().pixelRatio;
this.updateCanvasSize();
if (this.imgUrl) {
await this.loadImage(this.imgUrl);
}
});
}, },
applyTransform(matrix) { updateCanvasSize() {
if (!matrix) this.canvasDisplayWidth = this.width;
return; this.canvasDisplayHeight = this.height;
const pixelRatio = common_vendor.index.getSystemInfoSync().pixelRatio; this.canvasActualWidth = this.width * this.dpr;
const tx = matrix.tx * pixelRatio; this.canvasActualHeight = this.height * this.dpr;
const ty = matrix.ty * pixelRatio; if (this.canvas) {
this.ctx.setTransform( this.canvas.width = this.canvasActualWidth;
matrix.a, this.canvas.height = this.canvasActualHeight;
matrix.b, this.ctx.scale(this.dpr, this.dpr);
matrix.c, this.redraw();
matrix.d,
tx,
ty
);
},
// 调试网格
// drawDebugGrid() {
// if (!this.ctx) return;
// const pixelRatio = uni.getSystemInfoSync().pixelRatio;
// const width = this.canvasWidth * pixelRatio;
// const height = this.canvasHeight * pixelRatio;
// // 保存当前状态
// this.ctx.save();
// // 重置变换
// this.ctx.setTransform(1, 0, 0, 1, 0, 0);
// // 绘制网格
// this.ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
// this.ctx.lineWidth = 1;
// // 横向线
// for (let y = 0; y <= height; y += 50) {
// this.ctx.beginPath();
// this.ctx.moveTo(0, y);
// this.ctx.lineTo(width, y);
// this.ctx.stroke();
// }
// // 纵向线
// for (let x = 0; x <= width; x += 50) {
// this.ctx.beginPath();
// this.ctx.moveTo(x, 0);
// this.ctx.lineTo(x, height);
// this.ctx.stroke();
// }
// // 绘制坐标轴
// this.ctx.strokeStyle = '#ff0000';
// this.ctx.lineWidth = 2;
// this.ctx.beginPath();
// this.ctx.moveTo(0, 0);
// this.ctx.lineTo(width, 0);
// this.ctx.stroke();
// this.ctx.beginPath();
// this.ctx.moveTo(0, 0);
// this.ctx.lineTo(0, height);
// this.ctx.stroke();
// // 恢复之前的状态
// this.ctx.restore();
// this.ctx.draw();
// },
// 绘制主内容
draw() {
if (!this.ctx) {
common_vendor.index.__f__("warn", "at pages/index/transform-canvas.vue:110", "Canvas上下文未初始化");
return;
} }
this.clearCanvas();
this.applyTransform(this.matrix);
this.drawContent();
}, },
clearCanvas() { async loadImage(src) {
const pixelRatio = common_vendor.index.getSystemInfoSync().pixelRatio; try {
const width = this.canvasWidth * pixelRatio; this.image = await new Promise((resolve, reject) => {
const height = this.canvasHeight * pixelRatio; const image = this.canvas.createImage();
this.ctx.setTransform(1, 0, 0, 1, 0, 0); image.src = src;
this.ctx.clearRect(0, 0, width, height); image.onload = () => resolve(image);
image.onerror = reject;
});
this.redraw();
} catch (e) {
common_vendor.index.__f__("error", "at pages/index/transform-canvas.vue:125", "图片加载失败:", e);
this.image = null;
this.redraw();
}
}, },
drawContent() { redraw() {
this.ctx.fillStyle = "#3498db"; if (!this.ctx)
this.ctx.fillRect(50, 50, 100, 100); return;
this.ctx.fillStyle = "#e74c3c"; this.ctx.save();
this.ctx.beginPath(); this.ctx.resetTransform();
this.ctx.arc(150, 150, 50, 0, 2 * Math.PI); this.ctx.clearRect(0, 0, this.canvasActualWidth, this.canvasActualHeight);
this.ctx.fill(); this.ctx.scale(this.dpr, this.dpr);
this.ctx.fillStyle = "#2c3e50"; const { a, b, c, d, tx, ty } = this.matrix;
this.ctx.font = "20px sans-serif"; this.ctx.setTransform(a, b, c, d, tx, ty);
this.ctx.fillText("手势Canvas", 50, 250); if (this.image) {
this.ctx.draw(); this.ctx.drawImage(
this.image,
0,
0,
this.canvasDisplayWidth,
this.canvasDisplayHeight
);
} else {
this.ctx.fillStyle = "#f0f0f0";
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fillStyle = "#999";
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.fillText("图片加载失败", this.width / 2, this.height / 2);
}
this.ctx.restore();
} }
} }
}; };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return { return {
a: $data.canvasWidth + "px", a: `${$data.canvasDisplayWidth}px`,
b: $data.canvasHeight + "px" b: `${$data.canvasDisplayHeight}px`
}; };
} }
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]); const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@ -1 +1 @@
<canvas canvas-id="gestureCanvas" style="{{'width:' + a + ';' + ('height:' + b)}}"/> <view><canvas id="myCanvas" canvas-id="myCanvas" type="2d" style="{{'width:' + a + ';' + ('height:' + b) + ';' + ('border:' + '1px solid #eee')}}"></canvas></view>