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>
 |