Files
zgty-mas-m/uview-plus/components/u-canvas/u-canvas.vue
2026-03-05 16:54:47 +08:00

423 lines
12 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.
<template>
<view class="u-canvas"
:id="rootId"
:style="{
width: useRootHeightAndWidth ? '100%' : 'auto',
height: useRootHeightAndWidth ? '100%' : 'auto',
}">
<!-- #ifdef MP || H5 -->
<canvas
class="u-canvas__canvas"
:id="canvasId"
:canvas-id="canvasId"
type="2d"
:style="{ width: width + unit, height: height + unit }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"/>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<canvas
class="u-canvas__canvas"
:id="canvasId"
:canvas-id="canvasId"
:style="{ width: width + unit, height: height + unit }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"/>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<gcanvas class="u-canvas__canvas" ref="gcanvess"
:style="{ width: width + unit, height: height + unit }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd">
</gcanvas>
<!-- #endif -->
</view>
</template>
<script>
// #ifdef APP-NVUE
// https://github.com/dcloudio/NvueCanvasDemo/blob/master/README.md
import {
enable,
WeexBridge,
Image as GImage
} from '../../libs/util/gcanvas/index.js';
// #endif
let canvasNode = null;
export default {
name: "u-canvas",
props: {
canvasId: {
type: String,
default: () => {
return `u-canvas${Math.floor(Math.random() * 1000000)}`
}
},
width: {
type: [String, Number],
default: 300
},
height: {
type: [String, Number],
default: 300
},
unit: {
type: String,
default: 'px'
},
useRootHeightAndWidth: {
type: Boolean,
default: false
},
// 背景色
bgColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
rootId: `rootId${Number(Math.random() * 100).toFixed(0)}`,
ganvas: null,
canvasContext: null,
widthLocal: this.width,
heightLocal: this.height,
ctx: null
};
},
computed: {
// 计算实际画布尺寸
actualWidth() {
return this.useRootHeightAndWidth ? this.widthLocal : Number(this.width);
},
actualHeight() {
return this.useRootHeightAndWidth ? this.heightLocal : Number(this.height);
}
},
methods: {
// 添加触摸事件处理方法
onTouchStart(e) {
this.$emit('touchstart', e);
},
onTouchMove(e) {
this.$emit('touchmove', e);
},
onTouchEnd(e) {
this.$emit('touchend', e);
},
/**
* 获取节点
* @param id 节点id
* @param isCanvas 是否为Canvas节点
* @return {Promise<unknown>}
*/
async getCanvasNode(id, isCanvas = true) {
let that = this
return new Promise((resolve, reject) => {
try {
// #ifdef APP-NVUE
setTimeout(() => {
/*获取元素引用*/
this.ganvas = this.$refs["gcanvess"]
/*通过元素引用获取canvas对象*/
let canvasNode = enable(this.ganvas, {
bridge: WeexBridge
})
resolve(canvasNode)
}, 200)
// #endif
// #ifndef APP-PLUS-NVUE
const query = uni.createSelectorQuery().in(that).select(`#${id}`);
query.fields({
node: true,
size: true
})
.exec((res) => {
if (isCanvas) {
if (res[0]?.node) {
resolve(res[0].node)
} else {
resolve(false)
console.error("获取节点出错", res)
}
} else {
resolve(res[0])
}
})
// #endif
} catch (e) {
console.error("获取节点失败", e)
}
})
},
/**
* 获取Canvas上下文
*/
getCanvasContext() {
// #ifdef APP-PLUS
return uni.createCanvasContext(this.canvasId, this);
// #endif
// #ifdef APP-PLUS-NVUE || MP || H5
return canvasNode.getContext('2d');
// #endif
},
/**
* 初始化Canvas
*/
async initCanvas() {
try {
canvasNode = await this.getCanvasNode(this.canvasId);
// #ifdef MP-WEIXIN
// 在微信小程序中,为了提高清晰度,需要考虑设备像素比
const dpr = uni.getSystemInfoSync().pixelRatio;
if(canvasNode) {
// 设置canvas实际绘制尺寸为显示尺寸的dpr倍
canvasNode.width = this.actualWidth * dpr;
canvasNode.height = this.actualHeight * dpr;
}
// #endif
this.ctx = this.getCanvasContext();
// #ifdef MP-WEIXIN
if(this.ctx) {
this.ctx.scale(dpr, dpr);
}
// #endif
// 初始化背景但不在微信小程序中调用draw
this.clearCanvas();
} catch (error) {
console.error("初始化Canvas失败:", error);
}
},
/**
* 清空画布
*/
clearCanvas() {
if (!this.ctx) return;
this.clearRect(0, 0, this.actualWidth, this.actualHeight);
// 填充背景色
this.beginPath();
this.rect(0, 0, this.actualWidth, this.actualHeight);
this.setFillStyle(this.bgColor);
this.fill();
this.draw();
},
rect(x, y, width, height) {
if (!this.ctx) return;
this.ctx.rect(x, y, width, height);
},
clearRect(x, y, width, height) {
if (!this.ctx) return;
this.ctx.clearRect(x, y, width, height);
},
fill() {
if (!this.ctx) return;
this.ctx.fill();
},
setFillStyle(color) {
if (!this.ctx) return;
// #ifndef APP-PLUS-NVUE
if (this.ctx.setFillStyle) {
this.ctx.setFillStyle(color);
} else {
this.ctx.fillStyle = color;
}
// #endif
// #ifdef APP-PLUS-NVUE
this.ctx.setFillStyle(color);
// #endif
},
/**
* 设置线条样式
*/
setLineStyle(lineColor, lineWidth) {
if (!this.ctx) return;
this.setLineCap('round');
this.setLineJoin('round');
this.setStrokeStyle(lineColor);
this.setLineWidth(lineWidth);
},
setLineCap(lineCap = 'round') {
if (!this.ctx) return;
if (this.ctx.setLineCap) {
this.ctx.setLineCap(lineCap);
} else {
this.ctx.lineCap = lineCap;
}
},
setLineJoin(lineJoin = 'round') {
if (!this.ctx) return;
if (this.ctx.setLineJoin) {
this.ctx.setLineJoin(lineJoin);
} else {
this.ctx.lineJoin = lineJoin;
}
},
setStrokeStyle(color) {
if (!this.ctx) return;
if (this.ctx.setStrokeStyle) {
this.ctx.setStrokeStyle(color);
} else {
this.ctx.strokeStyle = color;
}
},
setLineWidth(width) {
if (!this.ctx) return;
if (this.ctx.setLineWidth) {
this.ctx.setLineWidth(width);
} else {
this.ctx.lineWidth = width;
}
},
/**
* 开始路径
*/
beginPath() {
if (!this.ctx) return;
this.ctx.beginPath();
},
/**
* 移动到某点
*/
moveTo(x, y) {
if (!this.ctx) return;
this.ctx.moveTo(x, y);
},
/**
* 画线到某点
*/
lineTo(x, y) {
if (!this.ctx) return;
this.ctx.lineTo(x, y);
},
/**
* 描边
*/
stroke() {
if (!this.ctx) return;
this.ctx.stroke();
},
/**
* 关闭路径
*/
closePath() {
if (!this.ctx) return;
this.ctx.closePath();
},
/**
* 绘制操作
*/
draw(isLastDraw = false) {
// #ifndef MP-WEIXIN
if (this.ctx && typeof this.ctx.draw === 'function') {
this.ctx.draw(isLastDraw);
}
// #endif
},
/**
* 导出图片
*/
exportImage(fileType = 'png', quality = 1) {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
// 微信小程序中需要先完成绘制,然后导出图片
setTimeout(() => {
uni.canvasToTempFilePath({
x: 0,
y: 0,
width: this.actualWidth,
height: this.actualHeight,
destWidth: this.actualWidth * 2, // 使用双倍尺寸以提高清晰度
destHeight: this.actualHeight * 2,
canvas: canvasNode, // 2d必须
canvasId: this.canvasId,
fileType: fileType,
quality: quality,
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => {
console.error('导出图片失败:', err);
reject(err);
}
}, this);
}, 50); // 等待50毫秒确保绘制完成
// #endif
// #ifndef MP-WEIXIN
uni.canvasToTempFilePath({
canvas: canvasNode, // 2d必须
canvasId: this.canvasId,
fileType: fileType,
quality: quality,
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => {
console.error('导出图片失败:', err);
reject(err);
}
}, this);
// #endif
});
},
/**
* 使用根节点宽高 设置新的size
* @return {Promise<void>}
*/
async setNewSize(){
const rootNode = await this.getCanvasNode(this.rootId, false);
const { width , height } = rootNode;
this.widthLocal = height;
this.heightLocal = width;
}
},
async mounted() {
// 如果使用根节点的宽高 则 重新设置 size
if(this.useRootHeightAndWidth){
await this.setNewSize();
}
// 初始化Canvas
await this.initCanvas();
}
};
</script>
<style lang="scss" scoped>
.u-canvas {
position: relative;
overflow: hidden;
}
.u-canvas__canvas {
display: block;
}
</style>