423 lines
12 KiB
Vue
423 lines
12 KiB
Vue
<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> |