<template> <view class="lime-painter" ref="limepainter"> <view v-if="canvasId && size" :style="styles"> <!-- #ifndef APP-NVUE --> <canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas> <canvas class="lime-painter__canvas" v-else :canvas-id="canvasId" :style="size" :id="canvasId" :width="boardWidth * dpr" :height="boardHeight * dpr"></canvas> <!-- #endif --> <!-- #ifdef APP-NVUE --> <web-view :style="size" ref="webview" src="/uni_modules/lime-painter/static/index.html" class="lime-painter__canvas" @pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage"> </web-view> <!-- #endif --> </view> <slot /> </view> </template> <script> import { parent } from '../common/relation' import props from './props' import {toPx, base64ToPath, pathToBase64, isBase64, sleep, getImageInfo}from './utils'; // #ifndef APP-NVUE import { compareVersion } from './utils'; import Painter from './painter' // import Painter from '@lime/' const nvue = {} // #endif // #ifdef APP-NVUE import nvue from './nvue' // #endif export default { name: 'lime-painter', mixins: [props, parent('painter'), nvue], data() { return { // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY use2dCanvas: true, // #endif // #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY use2dCanvas: false, // #endif canvasHeight: 150, canvasWidth: null, parentWidth: 0, inited: false, progress: 0, firstRender: 0, done: false }; }, computed: { styles() { return `${this.size}${this.customStyle||''};` }, canvasId() { return `l-painter${this._uid || this._.uid}` }, size() { if (this.boardWidth && this.boardHeight) { return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`; } }, dpr() { return this.pixelRatio || uni.getSystemInfoSync().pixelRatio; }, boardWidth() { const {width = 0} = (this.elements && this.elements.css) || this.elements || this const w = toPx(width||this.width) return w || Math.max(w, toPx(this.canvasWidth)); }, boardHeight() { const {height = 0} = (this.elements && this.elements.css) || this.elements || this const h = toPx(height||this.height) return h || Math.max(h, toPx(this.canvasHeight)); }, hasBoard() { return this.board && Object.keys(this.board).length }, elements() { return this.hasBoard ? this.board : JSON.parse(JSON.stringify(this.el)) } }, watch: { // #ifdef MP-WEIXIN || MP-ALIPAY size(v) { // #ifdef MP-WEIXIN if (this.use2dCanvas) { this.inited = false; } // #endif // #ifdef MP-ALIPAY this.inited = false; // #endif }, // #endif }, created() { const { SDKVersion, version, platform } = uni.getSystemInfoSync(); // #ifdef MP-WEIXIN this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !this.isPC; // #endif // #ifdef MP-TOUTIAO this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0; // #endif // #ifdef MP-ALIPAY this.use2dCanvas = this.type === '2d' && compareVersion(my.SDKVersion, '2.7.15') >= 0; // #endif }, async mounted() { await sleep(30) await this.getParentWeith() this.$nextTick(() => { setTimeout(() => { this.$watch('elements', this.watchRender, { deep: true, immediate: true }); }, 30) }) }, methods: { async watchRender(val, old) { if (!val || !val.views || (!this.firstRender ? !val.views.length : !this.firstRender) || !Object.keys(val).length || JSON.stringify(val) == JSON.stringify(old)) return; this.firstRender = 1 clearTimeout(this.rendertimer) this.rendertimer = setTimeout(() => { this.render(val); }, this.beforeDelay) }, async setFilePath(path, param) { let filePath = path const {pathType = this.pathType} = param || this if (pathType == 'base64' && !isBase64(path)) { filePath = await pathToBase64(path) } else if (pathType == 'url' && isBase64(path)) { filePath = await base64ToPath(path) } if (param && param.isEmit) { this.$emit('success', filePath); } return filePath }, async getSize(args) { const {width} = args.css || args const {height} = args.css || args if (!this.size) { if (width || height) { this.canvasWidth = width || this.canvasWidth this.canvasHeight = height || this.canvasHeight await sleep(30); } else { await this.getParentWeith() } } }, canvasToTempFilePathSync(args) { this.stopWatch = this.$watch('done', (v) => { if (v) { this.canvasToTempFilePath(args) this.stopWatch && this.stopWatch() } }, { immediate: true }) }, // #ifndef APP-NVUE getParentWeith() { return new Promise(resolve => { uni.createSelectorQuery() .in(this) .select(`.lime-painter`) .boundingClientRect() .exec(res => { const {width, height} = res[0]||{} this.parentWidth = Math.ceil(width||0) this.canvasWidth = this.parentWidth || 300 this.canvasHeight = height || this.canvasHeight||150 resolve(res[0]) }) }) }, async render(args = {}) { if(!Object.keys(args).length) { return console.error('空对象') } this.progress = 0 this.done = false await this.getSize(args) const ctx = await this.getContext(); let { use2dCanvas, boardWidth, boardHeight, canvas, afterDelay } = this; if (use2dCanvas && !canvas) { return Promise.reject(new Error('render: fail canvas has not been created')); } this.boundary = { top: 0, left: 0, width: boardWidth, height: boardHeight }; this.painter = null if (!this.painter) { const {width} = args.css || args const {height} = args.css || args if(!width && this.parentWidth) { Object.assign(args, {width: this.parentWidth}) } const param = { context: ctx, canvas, width: boardWidth, height: boardHeight, pixelRatio: this.dpr, useCORS: this.useCORS, createImage: getImageInfo.bind(this), listen: { onProgress: (v) => { this.progress = v this.$emit('progress', v) }, onEffectFail: (err) => { this.$emit('faill', err) } } } this.painter = new Painter(param) } // vue3 赋值给data会引起图片无法绘制 const { width, height } = await this.painter.source(JSON.parse(JSON.stringify(args))) this.boundary.height = this.canvasHeight = height this.boundary.width = this.canvasWidth = width await sleep(this.sleep); // 可能会因为尺寸改变影响绘制上下文 this.painter.setContext(this.ctx) await this.painter.render() await new Promise(resolve => this.$nextTick(resolve)); if (!use2dCanvas) { await this.canvasDraw(); } if (afterDelay && use2dCanvas) { await sleep(afterDelay); } this.$emit('done'); this.done = true if (this.isCanvasToTempFilePath) { this.canvasToTempFilePath() .then(res => { this.$emit('success', res.tempFilePath) }) .catch(err => { this.$emit('fail', new Error(JSON.stringify(err))); }); } return Promise.resolve({ ctx, draw: this.painter, node: this.node }); }, canvasDraw(flag = false) { return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this .afterDelay))); }, async getContext() { if (!this.canvasWidth) { this.$emit('fail', 'painter no size') console.error('painter no size: 请给画板或父级设置尺寸') return Promise.reject(); } if (this.ctx && this.inited) { return Promise.resolve(this.ctx); } const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this; const _getContext = () => { return new Promise(resolve => { uni.createSelectorQuery() .in(this) .select(`#${this.canvasId}`) .boundingClientRect() .exec(res => { if (res) { const ctx = uni.createCanvasContext(this.canvasId, this); if (!this.inited) { this.inited = true; this.use2dCanvas = false; this.canvas = res; } // #ifdef MP-ALIPAY ctx.scale(dpr, dpr); // #endif this.ctx = ctx resolve(this.ctx); } }); }); }; if (!use2dCanvas) { return _getContext(); } return new Promise(resolve => { uni.createSelectorQuery() .in(this) .select(`#${this.canvasId}`) .node() .exec(res => { let {node: canvas} = res[0]; if (!canvas) { this.use2dCanvas = false; resolve(this.getContext()); } const ctx = canvas.getContext(type); if (!this.inited) { this.inited = true; this.use2dCanvas = true; this.canvas = canvas; } this.ctx = ctx resolve(this.ctx); }); }); }, canvasToTempFilePath(args = {}) { return new Promise(async (resolve, reject) => { const { use2dCanvas, canvasId, dpr, fileType, quality } = this; const success = async (res) => { try { const tempFilePath = await this.setFilePath(res.tempFilePath || res) resolve(Object.assign(res, {tempFilePath})) } catch (e) { this.$emit('fail', e) } } let { top: y = 0, left: x = 0, width, height } = this.boundary || this; let destWidth = width * dpr; let destHeight = height * dpr; // #ifdef MP-ALIPAY width = destWidth; height = destHeight; // #endif const copyArgs = Object.assign({ x, y, width, height, destWidth, destHeight, canvasId, fileType, quality, success, fail: reject }, args); if (use2dCanvas) { try{ // #ifndef MP-ALIPAY if(!args.pathType && !this.pathType) {args.pathType = 'url'} const tempFilePath = await this.setFilePath(this.canvas.toDataURL(`image/${args.fileType||fileType}`.replace(/pg/, 'peg'), args.quality||quality), args) args.success && args.success({tempFilePath}) resolve({tempFilePath}) // #endif // #ifdef MP-ALIPAY this.canvas.toTempFilePath(copyArgs) // #endif }catch(e){ args.fail && args.fail(e) reject(e) } } else { // #ifdef MP-ALIPAY uni.canvasToTempFilePath(copyArgs); // #endif // #ifndef MP-ALIPAY uni.canvasToTempFilePath(copyArgs, this); // #endif } }) } // #endif } }; </script> <style> .lime-painter, .lime-painter__canvas { // #ifndef APP-NVUE width: 100%; // #endif // #ifdef APP-NVUE flex: 1; // #endif } </style>