462 lines
12 KiB
Vue
462 lines
12 KiB
Vue
|
<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 :id="canvasId" :canvas-id="canvasId" :style="size"
|
||
|
:width="boardWidth * dpr" :height="boardHeight * dpr" :hidpi="hidpi"></canvas>
|
||
|
|
||
|
<!-- #endif -->
|
||
|
<!-- #ifdef APP-NVUE -->
|
||
|
<web-view :style="size" ref="webview"
|
||
|
src="/uni_modules/lime-painter/hybrid/html/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 { canIUseCanvas2d, isPC} from './utils';
|
||
|
import Painter from './painter';
|
||
|
// import Painter from '@painter'
|
||
|
const nvue = {}
|
||
|
// #endif
|
||
|
// #ifdef APP-NVUE
|
||
|
import nvue from './nvue'
|
||
|
// #endif
|
||
|
export default {
|
||
|
name: 'lime-painter',
|
||
|
mixins: [props, parent('painter'), nvue],
|
||
|
data() {
|
||
|
return {
|
||
|
use2dCanvas: false,
|
||
|
canvasHeight: 150,
|
||
|
canvasWidth: null,
|
||
|
parentWidth: 0,
|
||
|
inited: false,
|
||
|
progress: 0,
|
||
|
firstRender: 0,
|
||
|
done: false,
|
||
|
tasks: []
|
||
|
};
|
||
|
},
|
||
|
computed: {
|
||
|
styles() {
|
||
|
return `${this.size}${this.customStyle||''};` + (this.hidden && 'position: fixed; left: 1500rpx;')
|
||
|
},
|
||
|
canvasId() {
|
||
|
return `l-painter${this._ && 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))
|
||
|
}
|
||
|
},
|
||
|
created() {
|
||
|
this.use2dCanvas = this.type === '2d' && canIUseCanvas2d() && !isPC
|
||
|
},
|
||
|
async mounted() {
|
||
|
await sleep(30)
|
||
|
await this.getParentWeith()
|
||
|
this.$nextTick(() => {
|
||
|
setTimeout(() => {
|
||
|
this.$watch('elements', this.watchRender, {
|
||
|
deep: true,
|
||
|
immediate: true
|
||
|
});
|
||
|
}, 30)
|
||
|
})
|
||
|
},
|
||
|
// #ifdef VUE3
|
||
|
unmounted() {
|
||
|
this.done = false
|
||
|
this.inited = false
|
||
|
this.firstRender = 0
|
||
|
this.progress = 0
|
||
|
this.painter = null
|
||
|
clearTimeout(this.rendertimer)
|
||
|
},
|
||
|
// #endif
|
||
|
// #ifdef VUE2
|
||
|
destroyed() {
|
||
|
this.done = false
|
||
|
this.inited = false
|
||
|
this.firstRender = 0
|
||
|
this.progress = 0
|
||
|
this.painter = null
|
||
|
clearTimeout(this.rendertimer)
|
||
|
},
|
||
|
// #endif
|
||
|
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
|
||
|
this.progress = 0
|
||
|
this.done = false
|
||
|
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.stopWatch()
|
||
|
// this.stopWatch = this.$watch('done', (v) => {
|
||
|
// if (v) {
|
||
|
// this.canvasToTempFilePath(args)
|
||
|
// this.stopWatch && this.stopWatch()
|
||
|
// }
|
||
|
// }, {
|
||
|
// immediate: true
|
||
|
// })
|
||
|
this.tasks.push(args)
|
||
|
if(this.done){
|
||
|
this.runTask()
|
||
|
}
|
||
|
},
|
||
|
runTask(){
|
||
|
while(this.tasks.length){
|
||
|
const task = this.tasks.shift()
|
||
|
this.canvasToTempFilePath(task)
|
||
|
}
|
||
|
},
|
||
|
// #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
|
||
|
// #ifdef APP-NVUE
|
||
|
this.tempFilePath.length = 0
|
||
|
// #endif
|
||
|
await this.getSize(args)
|
||
|
const ctx = await this.getContext();
|
||
|
|
||
|
let {
|
||
|
use2dCanvas,
|
||
|
boardWidth,
|
||
|
boardHeight,
|
||
|
canvas,
|
||
|
afterDelay
|
||
|
} = this;
|
||
|
if (use2dCanvas && !canvas) {
|
||
|
return Promise.reject(new Error('canvas 没创建'));
|
||
|
}
|
||
|
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),
|
||
|
performance: this.performance,
|
||
|
listen: {
|
||
|
onProgress: (v) => {
|
||
|
this.progress = v
|
||
|
this.$emit('progress', v)
|
||
|
},
|
||
|
onEffectFail: (err) => {
|
||
|
this.$emit('faill', err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.painter = new Painter(param)
|
||
|
}
|
||
|
try{
|
||
|
// 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);
|
||
|
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)));
|
||
|
});
|
||
|
}
|
||
|
this.runTask()
|
||
|
return Promise.resolve({
|
||
|
ctx,
|
||
|
draw: this.painter,
|
||
|
node: this.node
|
||
|
});
|
||
|
}catch(e){
|
||
|
//TODO handle the exception
|
||
|
}
|
||
|
|
||
|
},
|
||
|
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('[lime-painter]: 给画板或父级设置尺寸')
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// 钉钉小程序框架不支持 measureText 方法,用此方法 mock
|
||
|
if (!ctx.measureText) {
|
||
|
function strLen(str) {
|
||
|
let len = 0;
|
||
|
for (let i = 0; i < str.length; i++) {
|
||
|
if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
|
||
|
len++;
|
||
|
} else {
|
||
|
len += 2;
|
||
|
}
|
||
|
}
|
||
|
return len;
|
||
|
}
|
||
|
ctx.measureText = text => {
|
||
|
let fontSize = ctx.state && ctx.state.fontSize || 12;
|
||
|
const font = ctx.__font
|
||
|
if (font && fontSize == 12) {
|
||
|
fontSize = parseInt(font.split(' ')[3], 10);
|
||
|
}
|
||
|
fontSize /= 2;
|
||
|
return {
|
||
|
width: strLen(text) * fontSize
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// #ifdef MP-ALIPAY
|
||
|
ctx.scale(dpr, dpr);
|
||
|
// #endif
|
||
|
this.ctx = ctx
|
||
|
resolve(this.ctx);
|
||
|
} else {
|
||
|
console.error('[lime-painter] no node')
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
if (!use2dCanvas) {
|
||
|
return _getContext();
|
||
|
}
|
||
|
return new Promise(resolve => {
|
||
|
uni.createSelectorQuery()
|
||
|
.in(this)
|
||
|
.select(`#${this.canvasId}`)
|
||
|
.node()
|
||
|
.exec(res => {
|
||
|
let {node: canvas} = res && res[0]||{};
|
||
|
if(canvas) {
|
||
|
const ctx = canvas.getContext(type);
|
||
|
if (!this.inited) {
|
||
|
this.inited = true;
|
||
|
this.use2dCanvas = true;
|
||
|
this.canvas = canvas;
|
||
|
}
|
||
|
this.ctx = ctx
|
||
|
resolve(this.ctx);
|
||
|
} else {
|
||
|
console.error('[lime-painter]: no size')
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
},
|
||
|
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, args)
|
||
|
const result = Object.assign(res, {tempFilePath})
|
||
|
args.success && args.success(result)
|
||
|
resolve(result)
|
||
|
} 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,
|
||
|
id: canvasId,
|
||
|
fileType,
|
||
|
quality,
|
||
|
}, args, {success});
|
||
|
// if(this.isPC || use2dCanvas) {
|
||
|
// copyArgs.canvas = this.canvas
|
||
|
// }
|
||
|
if (use2dCanvas) {
|
||
|
copyArgs.canvas = this.canvas
|
||
|
try{
|
||
|
// #ifndef MP-ALIPAY
|
||
|
const oFilePath = this.canvas.toDataURL(`image/${args.fileType||fileType}`.replace(/pg/, 'peg'), args.quality||quality)
|
||
|
if(/data:,/.test(oFilePath)) {
|
||
|
uni.canvasToTempFilePath(copyArgs, this);
|
||
|
} else {
|
||
|
const tempFilePath = await this.setFilePath(oFilePath, 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
|
||
|
if(this.ctx.toTempFilePath) {
|
||
|
// 钉钉
|
||
|
const ctx = uni.createCanvasContext(canvasId);
|
||
|
ctx.toTempFilePath(copyArgs);
|
||
|
} else {
|
||
|
my.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>
|