uniapp 图片(二维码)分享到朋友圈

目录

    • @[TOC](目录)
      • 参考文档:
      • 应用:
      • HTML
      • JavaScript
      • css
      • index.js
      • util.js

引入 wxml2canvas 库,目录如下:
uniapp 图片(二维码)分享到朋友圈_第1张图片

参考文档:

wxml2canvas

文档地址:https://github.com/wg-front/wxml2canvas

应用:

HTML


		<view class="sharecontent" v-if="shareflag">
			<view id="sharetop" class="sharetop">
				<view style="position:relative;">
					<image class="canvas-draw" style="width:668rpx;height:812rpx ;" :src="picinfo.bg" data-type="image"
						:data-url="picinfo.bg">image>
					<rich-text style="position:absolute;top:120rpx;left:40rpx;" :nodes="richNodes">rich-text>
				view>
				<view style="box-sizing:border-box;min-height:150rpx;">
					<view style="font-size:31rpx;width:calc(100% - 150rpx)" data-type="text"
						:data-text="`我刚报名了${picinfo.school},快来看看吧`" class="canvas-draw">
						我刚报名了{{picinfo.school}},快来看看吧
					view>
					<image style="width:150rpx;height:150rpx;" data-type="image" :data-url="picinfo.codeimg"
						class="canvas-draw" :src="picinfo.codeimg">image>
				view>
				<view style="font-size:25rpx;" data-type="text" data-text="长按图片识别二维码,了解本校" class="canvas-draw">
					长按图片识别二维码,了解本校view>
			view>
			<view class="sharebottom">
				<view @click="changeImage" class="">
					<image src="../../static/share_haibao_0.png" mode="aspectFit">image>
					<view>更换图片view>
				view>
				<view @click="handleSaveToAlbum">
					<image src="../../static/share_haibao_3.png" mode="aspectFit">image>
					<view>保存到相册view>
				view>
			view>
			<view style="position:absolute;right:0;top:50upx;">
				<image src="../../static/pic868.png" mode="" style="width: 100upx;height: 100upx;" @click="close">
				image>
			view>
		view>

		<canvas style="width: 668rpx; height: 1032rpx;position:fixed;top:-2000rpx;z-index:-1;"
			canvas-id="canvas">canvas>

JavaScript

<script>
	import Wxml2Canvas from '@/utils/wxml2canvas/index.js';
	export default {
		data() {
			let school = getApp().globalData.school
			return {
				user: getApp().globalData.user,
				richNodes: [],
				picinfo: {
					logo: '../../static/img/logo.png',
					// school:school.name,
					school: "某某理工技师学院",
					bg: '',
					codeimg: ''
				},
				hasPhone: '',
				imageList: [],
				imgIndex: 0,
				shareflag: false,
				bg: '../../static/img/bg2.png',
				
				
			}
		},
		onLoad() {
			
			getQrCode({
				tenantId: this.tenantId,
				param: `shareId=${ this.user.id }`,
				page: 'pages/me/index'
			}).then(res => {
				let _this = this
				var shareQrImg = `data:image/jpg;base64,` + res.result
				
				this.base64src(shareQrImg, resCurrent => {
					    this.picinfo.codeimg = resCurrent
					    //resCurrent就是base64转换后的图片,直接给图片对的:src即可
				})
				
				
			})
			api.getPosterData({
				type: '1',
				tenantId: this.tenantId
			}).then(res => {
				this.imageList = res.result.map(item => item.content)
				this.picinfo.bg = this.imageList[this.imgIndex++]
			})
		},
		onShow() {
			this.user = getApp().globalData.user
		},
		methods: {
		
			handleSaveToAlbum() {
				uni.showLoading()
				const canvasId = 'canvas'
				let self = this;
				const query = uni.createSelectorQuery().in(this);
				query.select(`#sharetop`).boundingClientRect(data => {
					let sInfo = uni.getSystemInfoSync()
					let containerWidth = data.width,
						containerHeight = data.height;
					this.drawImage1 = new Wxml2Canvas({
						width: containerWidth, // 宽, 以iphone6为基准,传具体数值,其他机型自动适配
						height: containerHeight, // 高
						element: canvasId,
						background: '#fff',
						progress(percent) {},
						finish(url) {
							uni.hideLoading()
							uni.getSetting({
								success: res => {
									// 判断是否授权
									if (res.authSetting['scope.writePhotosAlbum']) {
										console.log(res.authSetting['scope.writePhotosAlbum'],
											'auth')
										uni.saveImageToPhotosAlbum({
											filePath: url,
											success(res) {
												uni.showToast({
													title: '名片已经保存到相册中',
												})
											},
											fail: function(err) {
												uni.showToast({
													title: '保存失败',
													icon: 'none'
												})
											}
										})
									} else {
										console.log(res.authSetting, 'authfalse')
										uni.authorize({
											scope: 'scope.writePhotosAlbum',
											success() {
												uni.saveImageToPhotosAlbum({
													filePath: url,
													success(res) {
														uni.showToast({
															title: '名片已经保存到相册中',
														})
													},
													fail: function(err) {
														uni.showToast({
															title: '保存失败',
															icon: 'none'
														})
													}
												})
											},
										})
									}
								}
							})

						},
						error(res) {
							console.log(res)
						}
					});
					let drawData = {
						list: [{
							type: 'wxml',
							class: '.sharetop .canvas-draw', // draw_canvas指定待绘制的元素
							limit: '.sharetop', // 限定绘制元素的范围,取指定元素与它的相对位置
							x: 0,
							y: 0
						}]
					}
					this.drawImage1.draw(drawData);
				}).exec();

			},
			close() {
				this.shareflag = false;
			},
			handlePhone(item, index) {
				this.hasPhone = getApp().globalData.user.phone
				this.viewDetail(item, index)
				console.log(item, 'item')
				console.log(index, 'index')
			},
			viewDetail(item) {
				console.log(item, '1111');
				if (item.name == "consultation" || item.name == "enrollInfo") {
					uni.switchTab({
						url: "/pages/" + item.name + "/index"
					})
				} else if (item.name == 'share1') {
					uni.share({
						provider: "weixin",
						scene: "WXSceneSession",
						type: 0,
						href: "http://uniapp.dcloud.io/",
						title: "西安理工技师学院分享",
						summary: "欢迎报考西安理工技师学院!",
						imageUrl: "https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/d8590190-4f28-11eb-b680-7980c8a877b8.png",
						success: function(res) {
							console.log("success:" + JSON.stringify(res));
						},
						fail: function(err) {
							console.log("fail:" + JSON.stringify(err));
						}
					});
				} else if (item.name == 'share2') {
					this.shareflag = true
				} else {
					console.log('aaa')
					uni.navigateTo({
						url: "/pages_me/" + item.name + "/index"
					})
				}
			},
			//将base64格式转化成图片格式
			base64src(base64data, cb){
				const fsm = wx.getFileSystemManager();
				const FILE_BASE_NAME = 'tmp_base64src'; //自定义文件名
				const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
				if (!format) {
					return (new Error('ERROR_BASE64SRC_PARSE'));
				}
				const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
				const buffer = wx.base64ToArrayBuffer(bodyData);
				fsm.writeFile({
					filePath,
					data: buffer,
					encoding: 'binary',
					success() {
						 cb(filePath);
						// _this.picinfo.codeimg=filePath
						// console.log(filePath);
					},
					fail() {
						return (new Error('ERROR_BASE64SRC_WRITE'));
					},
				});
			}
		}
	}
</script>

css


index.js

import Util from './util';

class Wxml2Canvas {
    constructor (options = {}) {
        this.device = wx.getSystemInfoSync && wx.getSystemInfoSync() || {};
        
        if (!options.zoom) {
            this.zoom = this.device.windowWidth / 375;
        } else {
            this.zoom = options.zoom || 1;
        }   
        
        this.element = options.element;
        this.object = options.obj;
        this.width = options.width * this.zoom || 0;
        this.height = options.height * this.zoom || 0;
        this.destWidth = options.destWidth || this.width * 2;
        this.destHeight = options.destHeight || this.height * 2;
        this.translateX = options.translateX * this.zoom || 0;
        this.translateY = options.translateY * this.zoom || 0;
        this.gradientBackground = options.gradientBackground || null;
        this.background = options.background || '#ffffff';
        this.finishDraw = options.finish || function finish(params) {}
        this.errorHandler = options.error || function error(params) {}
        this.progress = options.progress || function progress(params) {}
        this.textAlign = options.textAlign || 'left';
        this.fullText = options.fullText || false;
        this.font = options.font || '14px PingFang SC';

        this._init();
    }

    draw (data = {}, that) {
        let self = this;
        this.data = data;
        this.fef = that;

        this.progress(10);
        this._preloadImage(data.list).then((result) => {
            this.progress(30);
            self._draw();
        }).catch((res) => {
            self.errorHandler(res);
        })
    }

    measureWidth (text, font) {
        if(font) {
            this.ctx.font = font;
        }
        let res = this.ctx.measureText(text) || {};
        return res.width || 0;
    }

    _init () {
        this.progressPercent = 0;   // 绘制进度百分比
        this.data = null;
        this.ref = null;
        this.allPic = [];
        this.screenList = []; 
        this.asyncList = [];
        this.imgUrl = '';
        this.progressPercent = 0;
        this.distance = 0;
        this.progress(0);

        this.ctx = wx.createCanvasContext(this.element, this.obj);
        this.ctx.font = this.font;
        this.ctx.setTextBaseline('top');
        this.ctx.setStrokeStyle('white');

        this.debug = this.device.platform === 'devtools' ? true : false;

        this._drawBakcground();
    }

    _drawBakcground () {
        if (this.gradientBackground) {
            let line = this.gradientBackground.line || [0, 0, 0, this.height];
            let color = this.gradientBackground.color || ['#fff', '#fff'];
            let style = { fill: { line, color } }
            this._drawRectToCanvas(0, 0, this.width, this.height, style);
        } else {
            let style = { fill: this.background }
            this._drawRectToCanvas(0, 0, this.width, this.height, style);
        }
    }

    _draw () {
        let self = this;
        let list = this.data.list || [];
        let index = 0;
        let all = [];
        let count = 0;

        list.forEach(item => {
            if(item.type === 'wxml') {
                count += 3;
            } else {
                count += 1;
            }
        })

        this.distance = 60 / (count || 1); // 进度条的间距
        this.progressPercent = 30;
        this.asyncList = list.filter( item => item.delay == true );
        list = list.filter( item => item.delay != true );
        drawList(list);

        Promise.all(all).then(results => {
            index = 0;
            drawList(self.asyncList, true);

            Promise.all(all).then(results => {
                self.progress(90);
                self._saveCanvasToImage();
            });
        }).catch (e => {
            console.log(e)
            self.errorHandler(e);
        });

        function drawList(list = [], noDelay) {
            list.forEach((item, i) => {
                all[index++] = new Promise((resolve, reject) => {
                    let attr = item.style;
                    item.progress = self.distance;
                    if (noDelay) {
                        item.delay = 0;
                    }
                    if (item.type === 'radius-image') {
                        self._drawCircle(item, attr, resolve, reject, 'image');
                    } else if (item.type === 'text') {
                        self._drawText(item, attr, resolve, reject);
                    } else if (item.type === 'line') {
                        self._drawLine(item, attr, resolve, reject);
                    } else if (item.type === 'circle') {
                        self._drawCircle(item, attr, resolve, reject);
                    } else if (item.type === 'rect') {
                        self._drawRect(item, attr, resolve, reject);
                    } else if (item.type === 'image') {
                        self._drawRect(item, attr, resolve, reject, 'image');
                    } else if (item.type === 'wxml') {
                        self._drawWxml(item, attr, resolve, reject);
                    }else {
                        resolve();
                    }
                });
            });
        }
    }

    _saveCanvasToImage () {
        let self = this;       

        // 延时保存有两个原因,一个是等待绘制delay的元素,另一个是安卓上样式会错乱
        setTimeout(() => {
            self.progress(95);

            wx.canvasToTempFilePath({
                x: 0,
                y: 0,
                width: self.width,
                height: self.height,
                destWidth: self.destWidth,
                destHeight: self.destHeight,
                canvasId: self.element,
                success: function (res) {

                    self.progress(100);
                    self.imgUrl = res.tempFilePath;
                    self.finishDraw(self.imgUrl);
                },
                fail: function (res) {

                    self.errorHandler({errcode: 1000, errmsg: 'save canvas error', e: res});
                }
            }, self.object);
        }, self.device.system.indexOf('iOS') === -1 ? 300 : 100);
    }

    _preloadImage (list = []) {
        let self = this;
        let all = [];
        let count = 0;

        list.forEach((item, i) => {
            if (item.url && self._findPicIndex(item.url) === -1) {
                // 避免重复下载同一图片
            self.allPic.push({
                url: item.url,
                local: ''
            });
            all[count++] = new Promise((resolve, reject) => {
                // 非http(s)域名的就不下载了
                if (!/^http/.test(item.url) || /^http:\/\/tmp\//.test(item.url) || /^http:\/\/127\.0\.0\.1/.test(item.url)) {
                    let index = self._findPicIndex(item.url);
                    if(index > -1) {
                        self.allPic[index].local = item.url;
                    }
                    resolve({ tempFilePath: item.url });
                } else {
                    wx.downloadFile({
                        url: item.url.replace(/^https?/, 'https'),
                        success: function (res) {
                            let index = self._findPicIndex(item.url);
                            if (index > -1) {
                                self.allPic[index].local = res.tempFilePath;
                            }
                            resolve(res);
                        },
                        fail: (res) => {
                            reject({errcode: 1001, errmsg: 'download pic error'});
                        }
                    })
                }
            }) 
            }
        });

        return Promise.all(all).then(results => {
            return new Promise(resolve => { resolve() })
        }).catch((results) => {
            return new Promise((resolve, reject) => { reject(results) })
        })
    }

    _findPicIndex (url) {
        let index = this.allPic.findIndex(pic => pic.url === url);
        return index;
    }

    _drawRect (item, style, resolve, reject, isImage, isWxml) {
        let zoom = this.zoom;
        let leftOffset = 0;
        let topOffset = 0;
        let width = style.width;
        let height = style.height;
        try {
            item.x = this._resetPositionX(item, style);
            item.y = this._resetPositionY(item, style);
            
            let url;
            if(isImage) {
                let index = this._findPicIndex(item.url);
                url = index > -1 ? this.allPic[index].local : item.url;
            }
            
            style.padding = style.padding || [];
            if(isWxml === 'inline-wxml') {
                item.x = item.x + (style.padding[3] && style.padding[3] || 0)
                item.y = item.y + (style.padding[0] && style.padding[0] || 0)
            }

            leftOffset = item.x + style.width + (style.padding[1] && style.padding[1] || 0);

            if(!isWxml) {
                width = width * zoom;
                height = height * zoom;
            }
            this._drawRectToCanvas(item.x, item.y, width, height, style, url);
            this._updateProgress(item.progress);

            if(resolve) {
                resolve();
            }else {
                return {
                    leftOffset,
                    topOffset
                }
            }
        } catch (e) {
            reject && reject({ errcode: (isImage ? 1003 : 1002), errmsg: (isImage ? 'drawImage error' : 'drawRect error'), e });
        }
    }

    _drawRectToCanvas(x, y, width, height, style, url) {
        let { fill, border, boxShadow } = style;

        this.ctx.save();
        this._drawBoxShadow(boxShadow, (res) => {
            // 真机上填充渐变色时,没有阴影,先画个相等大小的纯色矩形来实现阴影
            if(fill && typeof fill !== 'string' && !this.debug) {
                this.ctx.setFillStyle(res.color || '#ffffff');
                this.ctx.fillRect(x, y, width, height);
            }
        });


        if(url) {
            this.ctx.drawImage(url, x, y, width, height)
        }else {
            this._setFill(fill, () => {
                this.ctx.fillRect(x, y, width, height);
            });
        }

        this._drawBorder(border, style, (border) => {
            let fixBorder = border.width;
            this.ctx.strokeRect(x - fixBorder / 2, y - fixBorder / 2, width + fixBorder, height + fixBorder);
        });

        this.ctx.draw(true);
        this.ctx.restore();
    }

    _drawText (item, style, resolve, reject, type, isWxml) {
        let zoom = this.zoom;
        let leftOffset = 0;
        let topOffset = 0;

        try {
            style.fontSize = this._parseNumber(style.fontSize);
            let fontSize = Math.ceil((style.fontSize || 14) * zoom)
            this.ctx.setTextBaseline('top');
            this.ctx.font = (`${style.fontWeight ? (style.fontWeight) : 'normal'} ${ fontSize }px ${ style.fontFamily || 'PingFang SC' }`);
            this.ctx.setFillStyle(style.color || '#454545');

            let text = item.text || '';
            let textWidth = Math.floor(this.measureWidth(text, style.font || this.ctx.font));
            let lineHeight = this._getLineHeight(style);
            let textHeight = Math.ceil(textWidth / (style.width || textWidth)) * lineHeight;
            let width = Math.ceil((style.width || textWidth) * (!isWxml ? zoom : 1));
            let whiteSpace = style.whiteSpace || 'wrap';
            let x = 0;
            let y = 0;                

            if(typeof style.padding === 'string') {
                style.padding = Util.transferPadding(style.padding);
            }
            item.x = this._resetPositionX(item, style);
            item.y = this._resetPositionY(item, style, textHeight);
            this._drawBoxShadow(style.boxShadow);

            if(style.background || style.border) {
                this._drawTextBackgroud(item, style, textWidth, textHeight, isWxml);
            }
            
            // 行内文本
            if(type === 'inline-text') {
                width = item.maxWidth;
                if(item.leftOffset + textWidth > width) {
                    // 如果上一个行内元素换行了,这个元素要继续在后面补足一行
                    let lineNum = Math.max(Math.floor(textWidth / width), 1);
                    let length = text.length;
                    let singleLength = Math.floor(length / lineNum);
                    let widthOffset = item.leftOffset ? item.leftOffset - item.originX : 0;
                    let { endIndex: currentIndex, single, singleWidth } = this._getTextSingleLine(text, width, singleLength, 0, widthOffset)
                    x = this._resetTextPositionX(item, style, singleWidth);
                    y = this._resetTextPositionY(item, style);
                    this.ctx.fillText(single, x, y);
                    leftOffset = x + singleWidth;
                    topOffset = y;

                    // 去除第一行补的内容,然后重置
                    text = text.substring(currentIndex, text.length);
                    currentIndex = 0;
                    lineNum = Math.max(Math.floor(textWidth / width), 1);
                    textWidth = Math.floor(this.measureWidth(text, style.font || this.ctx.font));
                    item.x = item.originX; // 还原换行后的x
                    for (let i = 0; i < lineNum; i++) {
                        let { endIndex, single, singleWidth } = this._getTextSingleLine(text, width, singleLength, currentIndex);
                        currentIndex = endIndex;
                        if(single) {
                            x = this._resetTextPositionX(item, style, singleWidth, width);
                            y = this._resetTextPositionY(item, style, i + 1);
                            this.ctx.fillText(single, x, y);
                            if(i === lineNum - 1) {
                                leftOffset = x + singleWidth;
                                topOffset = lineHeight * lineNum;
                            }
                        }
                    }

                    let last = text.substring(currentIndex, length);
                    let lastWidth = this.measureWidth(last);

                    if(last) {
                        x = this._resetTextPositionX(item, style, lastWidth, width);
                        y = this._resetTextPositionY(item, style, lineNum + 1);
                        this.ctx.fillText(last, x, y);
                        leftOffset = x + lastWidth;
                        topOffset = lineHeight * (lineNum + 1);
                    }
                }else {
                    x = this._resetTextPositionX(item, style, textWidth, width);
                    y = this._resetTextPositionY(item, style);
                    this.ctx.fillText(item.text, x, y);
                    leftOffset = x + textWidth;
                    topOffset = lineHeight;
                }
            }else {
                // block文本,如果文本长度超过宽度换行
                if (width && textWidth > width && whiteSpace !== 'nowrap') {
                    let lineNum = Math.max(Math.floor(textWidth / width), 1);
                    let length = text.length;
                    let singleLength = Math.floor(length / lineNum);
                    let currentIndex = 0;

                    // lineClamp参数限制最多行数
                    if (style.lineClamp && lineNum + 1 > style.lineClamp) {
                        lineNum = style.lineClamp - 1;
                    }

                    for (let i = 0; i < lineNum; i++) {
                        let { endIndex, single, singleWidth } = this._getTextSingleLine(text, width, singleLength, currentIndex);
                        currentIndex = endIndex;
                        x = this._resetTextPositionX(item, style, singleWidth, width);
                        y = this._resetTextPositionY(item, style, i);
                        this.ctx.fillText(single, x, y);

                    }

                    // 换行后剩余的文字,超过一行则截断增加省略号
                    let last = text.substring(currentIndex, length);
                    let lastWidth = this.measureWidth(last);
                    if(lastWidth > width) {
                        let { single, singleWidth } = this._getTextSingleLine(last, width, singleLength);
                        lastWidth = singleWidth;
                        last = single.substring(0, single.length - 1) + '...';
                    }

                    x = this._resetTextPositionX(item, style, lastWidth, width);
                    y = this._resetTextPositionY(item, style, lineNum);
                    this.ctx.fillText(last, x, y);

                }else {
                    x = this._resetTextPositionX(item, style, textWidth, width);
                    y = this._resetTextPositionY(item, style);
                    this.ctx.fillText(item.text, x, y);
                }
            }
            
            this.ctx.draw(true);
            
            this._updateProgress(item.progress);

            if(resolve) {
                resolve();
            }else {
                return {
                    leftOffset,
                    topOffset
                }
            }
        } catch(e) {
            reject && reject({ errcode: 1004, errmsg: 'drawText error', e: e });
        }
    }

    _drawTextBackgroud (item, style, textWidth, textHeight, isWxml) {
        if(!style.width) return;
        let zoom = isWxml ? 1 : this.zoom;
        let width = style.width || textWidth;
        let height = style.height || textHeight;
        let rectStyle = {
            fill: style.background,
            border: style.border
        }
        style.padding = style.padding || [0, 0, 0, 0];
        width += (style.padding[1] || 0) + (style.padding[3] || 0);
        height += (style.padding[0] || 0) + (style.padding[2] || 0);
        width = width * zoom
        height = height * zoom
        this._drawRectToCanvas(item.x, item.y, width, height, rectStyle);
    }

    _drawCircle (item, style, resolve, reject, isImage, isWxml) {
        let zoom = this.zoom;
        let r = style.r;
        try {
            
            item.x = this._resetPositionX(item, style);
            item.y = this._resetPositionY(item, style);

            let url;
            if(isImage) {
                url = item.url;
            }

            if(!isWxml) {
                r = r * zoom;
            }

            this._drawCircleToCanvas(item.x, item.y, r, style, url);
            
            this._updateProgress(item.progress);
            resolve && resolve();
        } catch (e) {
            reject && reject({ errcode: (isImage ? 1006 : 1005), errmsg: (isImage ? 'drawCircleImage error' : 'drawCircle error'), e });
        }
    }

    _drawCircleToCanvas (x, y, r, style, url) {
        let { fill, border, boxShadow } = style;

        this.ctx.save();

        this._drawBoxShadow(boxShadow, (res) => {
            // 真机上填充渐变色时,没有阴影,先画个相等大小的纯色矩形来实现阴影
            if((fill && typeof fill !== 'string') || (url && res.color)) {
                this.ctx.setFillStyle(res.color || '#ffffff');
                this.ctx.beginPath();
                this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI);
                this.ctx.closePath();
                this.ctx.fill();
            }
        });

        if(url) {
            this.ctx.save();
            this.ctx.beginPath();
            this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI);
            this.ctx.clip();
            this.ctx.drawImage(url, x, y, r * 2, r * 2);
            this.ctx.closePath();
            this.ctx.restore();
        }else {
            this._setFill(fill, () => {
                this.ctx.beginPath();
                this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI);
                this.ctx.closePath();
                this.ctx.fill();
            });
        }

        this._drawBorder(border, style, (border) => {
            this.ctx.beginPath()
            this.ctx.arc(x + r, y + r, r + border.width / 2, 0, 2 * Math.PI)
            this.ctx.stroke()
            this.ctx.closePath();
        });

        this.ctx.draw(true);
        this.ctx.restore();
    }

    _drawLine (item, style, resolve, reject, isWxml) {
        let zoom = this.zoom;
        try {
            let x1 = item.x * zoom + this.translateX;
            let y1 = item.y * zoom + this.translateY;
            let x2 = item.x2 * zoom + this.translateX;
            let y2 = item.y2 * zoom + this.translateY;
            this._drawLineToCanvas(x1, y1, x2, y2, style);

            this._updateProgress(item.progress);
            resolve && resolve();
        } catch (e) {
            reject && reject({ errcode: 1007, errmsg: 'drawLine error', e });
        }
    }

    _drawLineToCanvas (x1, y1, x2, y2, style) {
        let { stroke, dash, boxShadow } = style;

        this.ctx.save();
        if(stroke) {
            this._setStroke(stroke);
        }

        this._drawBoxShadow(boxShadow);

        if(dash) {
            let dash = [style.dash[0] || 5, style.dash[1] || 5];
            let offset = style.dash[2] || 0;
            this.ctx.setLineDash(dash, offset || 0);
        }

        this.ctx.moveTo(x1, y1);
        this.ctx.setLineWidth((style.width || 1) * this.zoom);
        this.ctx.lineTo(x2, y2);
        this.ctx.stroke();
        this.ctx.draw(true);
        this.ctx.restore();
    }

    // 废弃,合并到_drawRect
    _drawImage (item, style, resolve, reject, isWxml) {
        let zoom = this.zoom;
        try {
            
            item.x = this._resetPositionX(item, style);
            item.y = this._resetPositionY(item, style);
            item.x = item.x + (style.padding[3] || 0);
            item.y = item.y + (style.padding[0] || 0);

            let index = this._findPicIndex(item.url);
            let url = index > -1 ? this.allPic[index].local : item.url;
            this._drawImageToCanvas(url, item.x, item.y, style.width * zoom, style.height * zoom, style);

            this._updateProgress(item.progress);
            resolve && resolve();
        } catch (e) {
            reject && reject({ errcode: 1012, errmsg: 'drawRect error', e });
        }
    }

    // 废弃,合并到_drawRect
    _drawImageToCanvas (url, x, y, width, height, style) {
        let { fill, border, boxShadow } = style;
        this.ctx.save();

        this._drawBoxShadow(boxShadow);
        this.ctx.drawImage(url, x, y, width, height);

        this._drawBorder(border, style, (border) => {
            let fixBorder = border.width;
            this.ctx.strokeRect(x - fixBorder / 2, y - fixBorder / 2, width + fixBorder, height + fixBorder);
        });
        this.ctx.draw(true);
        this.ctx.restore();
    }

    _drawWxml (item, style, resolve, reject) {
        let self = this;
        let all = [];
        try {
            this._getWxml(item, style).then((results) => {
                
                // 上 -> 下
                let sorted = self._sortListByTop(results[0]);
                let count = 0;
                let progress = 0;
                Object.keys(sorted).forEach(item => {
                    count += sorted[item].length;
                })
                progress = this.distance * 3 / (count || 1);

                all = this._drawWxmlBlock(item, sorted, all, progress, results[1]);
                all = this._drawWxmlInline(item, sorted, all, progress, results[1]);

                Promise.all(all).then(results => {
                    resolve && resolve();
                }).catch (e => {
                    reject && reject(e);
                });
            });
        } catch (e) {
            reject && reject({ errcode: 1008, errmsg: 'drawWxml error' });
        }
    }

    _drawWxmlBlock (item, sorted, all, progress, results) {
        let self = this;
        // 用来限定位置范围,取相对位置
        let limitLeft = (results ? results.left : 0);
        let limitTop = (results ? results.top : 0);
        Object.keys(sorted).forEach((top, topIndex) => {
            // 左 -> 右
            let list = sorted[top].sort((a, b) => {
                return (a.left - b.left);
            });

            list = list.filter(sub => sub.dataset.type && sub.dataset.type.indexOf('inline') === -1);

            list.forEach((sub, index) => {
                all[index] = new Promise((resolve2, reject2) => {
                    sub = self._transferWxmlStyle(sub, item, limitLeft, limitTop);
                    sub.progress = progress;
                    let type = sub.dataset.type;
                    if(sub.dataset.delay) {
                        setTimeout(() => {
                            drawWxmlItem();
                        }, sub.dataset.delay)
                    } else {
                        drawWxmlItem();
                    }
                    function drawWxmlItem () {
                        if (type === 'text') {
                            self._drawWxmlText(sub, resolve2, reject2);
                        } else if (type === 'image') {
                            self._drawWxmlImage(sub, resolve2, reject2);
                        } else if (type === 'radius-image') {
                            self._drawWxmlCircleImage(sub, resolve2, reject2);
                        } else if (type === 'background-image') {
                            self._drawWxmlBackgroundImage(sub, resolve2, reject2);
                        }
                    }
                });
            });
        });

        return all;

    }

    _drawWxmlInline (item, sorted, all, progress, results) {
        let self = this;
        let topOffset = 0;
        let leftOffset = 0;
        let lastTop = 0;
        let limitLeft = (results ? results.left : 0);
        let limitTop = (results ? results.top : 0);
        let p = new Promise((resolve2, reject2) => {
            let maxWidth = 0;
            let minLeft = Infinity;
            let maxRight = 0;

            // 找出同一top下的最小left和最大right,得到最大的宽度,用于换行
            Object.keys(sorted).forEach(top => {
                let inlineList = sorted[top].filter(sub => sub.dataset.type && sub.dataset.type.indexOf('inline') > -1);
                inlineList.forEach(sub => {
                        if(sub.left < minLeft) {
                        minLeft = sub.left
                    }
                    if(sub.right > maxRight) {
                        maxRight = sub.right;
                    }
                })
            });
            maxWidth = Math.ceil((maxRight - minLeft) || self.width);

            Object.keys(sorted).forEach((top, topIndex) => {
                // 左 -> 右
                let list = sorted[top].sort((a, b) => {
                    return (a.left - b.left);
                });

                // 换行的行内元素left放到后面,version2.0.6后无法获取高度,改用bottom值来判断是否换行了
                let position = -1;
                for(let i = 0, len = list.length; i < len; i++) {
                    if(list[i] && list[i + 1]) {
                        if(list[i].bottom > list[i + 1].bottom) {
                            position = i;
                            break;
                        }
                    }
                }

                if(position > -1) {
                    list.push(list.splice(position, 1)[0]);
                }

                let inlineList = list.filter(sub => sub.dataset.type && sub.dataset.type.indexOf('inline') > -1);
                let originLeft = (inlineList[0] ? inlineList[0].left : 0);
                // 换行后和top不相等时,认为是换行了,要清除左边距;当左偏移量大于最大宽度时,也要清除左边距; 当左偏移小于左边距时,也要清除
                if (Math.abs(topOffset + lastTop - top) > 2 || leftOffset - originLeft - limitLeft >= maxWidth || leftOffset <= originLeft - limitLeft - 2) {
                    leftOffset = 0;
                }

                lastTop = +top;
                topOffset = 0;
                
                inlineList.forEach((sub, index) => {
                    sub = self._transferWxmlStyle(sub, item, limitLeft, limitTop);
                    sub.progress = progress;
                    let type = sub.dataset.type;
                    if (type === 'inline-text') {
                        let drawRes = self._drawWxmlInlineText(sub, leftOffset, maxWidth);
                        leftOffset = drawRes.leftOffset;
                        topOffset = drawRes.topOffset;
                    } else if (type === 'inline-image') {
                        let drawRes = self._drawWxmlImage(sub);
                        leftOffset = drawRes.leftOffset;
                        topOffset = drawRes.topOffset;
                    }
                });
            });
            resolve2();
        })

        all.push(p); 
        return all;
    }

    _drawWxmlInlineText (sub, leftOffset = 0, maxWidth) {
        let text = sub.dataset.text || '';
        if(sub.dataset.maxlength && text.length > sub.dataset.maxlength) {
            text = text.substring(0, sub.dataset.maxlength) + '...';
        }
        
        let textData = {
            text,
            originX: sub.left,
            x: leftOffset ? leftOffset : sub.left,
            y: sub.top,
            progress: sub.progress,
            leftOffset: leftOffset,
            maxWidth: maxWidth // 行内元素的最大宽度,取决于limit的宽度
        }

        if (sub.backgroundColor !== 'rgba(0, 0, 0, 0)') {
            sub.background = sub.backgroundColor;
        }else {
            sub.background = 'rgba(0, 0, 0, 0)';
        }

        if(sub.dataset.background) {
            sub.background = sub.dataset.background;
        }

        let res = this._drawText(textData, sub, null, null, 'inline-text', 'wxml');

        return res
    }

    _drawWxmlText (sub, resolve, reject) {
        let text = sub.dataset.text || '';
        if(sub.dataset.maxlength && text.length > sub.dataset.maxlength) {
            text = text.substring(0, sub.dataset.maxlength) + '...';
        }
        
        let textData = {
            text,
            x: sub.left,
            y: sub.top,
            progress: sub.progress
        }
        if (sub.backgroundColor !== 'rgba(0, 0, 0, 0)') {
            sub.background = sub.backgroundColor;
        }else {
            sub.background = 'rgba(0, 0, 0, 0)';
        }

        if(sub.dataset.background) {
            sub.background = sub.dataset.background;
        }

        this._drawText(textData, sub, resolve, reject, 'text', 'wxml');
    }

    _drawWxmlImage (sub, resolve, reject) {
        let imageData = {
            url: sub.dataset.url,
            x: sub.left,
            y: sub.top,
            progress: sub.progress
        }

        let res = this._drawRect(imageData, sub, resolve, reject, 'image', 'inline-wxml');

        return res
    }

    _drawWxmlCircleImage (sub, resolve, reject) {
        let imageData = {
            url: sub.dataset.url,
            x: sub.left,
            y: sub.top,
            progress: sub.progress
        }
        sub.r = sub.width / 2;

        this._drawCircle(imageData, sub, resolve, reject, true, 'wxml');
    }

    _drawWxmlBackgroundImage (sub, resolve, reject) {
        let url = sub.backgroundImage.replace(/url\((\"|\')?/, '').replace(/(\"|\')?\)$/, '');
        let index = this._findPicIndex(url);
        url = index > -1 ? this.allPic[index].local : url;
        let size = sub.backgroundSize.replace(/px/g, '').split(' ');

        let imageData = {
            url: url,
            x: sub.left,
            y: sub.top,
            progress: sub.progress
        }

        this._drawRect(imageData, sub, resolve, reject, 'image', 'wxml');
    }

    _getWxml (item, style) {
        let self = this;
        let query;
        if(this.obj) {
            query = wx.createSelectorQuery().in(this.obj);
        }else {
            query = wx.createSelectorQuery();
        }

        let p1 = new Promise((resolve, reject) => {
            // 会触发两次,要限制
            let count = 0;
            query.selectAll(`${item.class}`).fields({
                dataset: true,
                size: true,
                rect: true,
                computedStyle: ['width', 'height', 'font', 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'textAlign', 
                    'color', 'lineHeight', 'border', 'borderColor', 'borderStyle', 'borderWidth',  'verticalAlign', 'boxShadow',
                    'background', 'backgroundColor', 'backgroundImage', 'backgroundPosition', 'backgroundSize', 'paddingLeft', 'paddingTop',
                    'paddingRight', 'paddingBottom'
                    ]
            }, (res) => {
                if(count++ === 0) {
                    let list = self._formatImage(res);
                    self._preloadImage(list).then(result => {
                        resolve(res);
                    }).catch((res) => {
                        reject && reject({ errcode: 1009, errmsg: 'drawWxml preLoadImage error' });
                    });
                }
            }).exec();
        });

        let p2 = new Promise((resolve, reject) => {
            if (!item.limit) {
                resolve({ top: 0, width: self.width / self.zoom });
            }

            query.select(`${item.limit}`).fields({
                dataset: true,
                size: true,
                rect: true,
            }, (res) => {
                resolve(res);
            }).exec();
        });

        return Promise.all([p1, p2]);
    }

    _getLineHeight (style) {
        let zoom = this.zoom;
        if(style.dataset && style.dataset.type) {
            zoom = 1;
        }
        let lineHeight;
        if(!isNaN(style.lineHeight) && style.lineHeight > style.fontSize) {
            lineHeight = style.lineHeight;
        }else {
            style.lineHeight = (style.lineHeight || '') + '';
            lineHeight = +style.lineHeight.replace('px', '');
            lineHeight = lineHeight ? lineHeight : (style.fontSize || 14) * 1.2;
        }
        return lineHeight * zoom;
    }

    _formatImage (res = []) {
        let list = [];
        res.forEach((item, index) => {
            let dataset = item.dataset;
            if(dataset.type === 'image' && dataset.url) {
                list.push({
                    url: dataset.url
                })
            } else if (dataset.type === 'background-image' && item.backgroundImage.indexOf('url') > -1) {
                let url = item.backgroundImage.replace(/url\((\"|\')?/, '').replace(/(\"|\')?\)$/, '');
                list.push({
                    url: url
                })
            }
        });

        return list;
    }

    _updateProgress (distance) {
        this.progressPercent += distance;
        this.progress(this.progressPercent);
    }

    _sortListByTop (list = []) {
        let sorted = {};

        // 粗略地认为2px相差的元素在同一行
        list.forEach((item, index) => {
            let top = item.top;
            if (!sorted[top]) {
                if (sorted[top - 2]) {
                    top = top - 2;
                }else if (sorted[top - 1]) {
                    top = top - 1;
                } else if (sorted[top + 1]) {
                    top = top + 1;
                } else if (sorted[top + 2]) {
                    top = top + 2;
                } else {
                    sorted[top] = [];
                }
            }
            sorted[top].push(item);
        });

        return sorted;
    }

    _parseNumber (number) {
        return isNaN(number) ? +(number || '').replace('px', '') : number;
    }

    _transferWxmlStyle (sub, item, limitLeft, limitTop) {
        let leftFix = (+sub.dataset.left || 0);
        let topFix = (+sub.dataset.top || 0);

        sub.width = this._parseNumber(sub.width);
        sub.height = this._parseNumber(sub.height);
        sub.left = this._parseNumber(sub.left) - limitLeft + (leftFix + (item.x || 0)) * this.zoom;
        sub.top = this._parseNumber(sub.top) - limitTop + (topFix + (item.y || 0)) * this.zoom;

        let padding = sub.dataset.padding || '0 0 0 0';
        if (typeof padding === 'string') {
            padding = Util.transferPadding(padding);
        }
        let paddingTop = Number(sub.paddingTop.replace('px', '')) + Number(padding[0]);
        let paddingRight = Number(sub.paddingRight.replace('px', '')) + Number(padding[1]);
        let paddingBottom = Number(sub.paddingBottom.replace('px', '')) + Number(padding[2]);
        let paddingLeft = Number(sub.paddingLeft.replace('px', '')) + Number(padding[3]);
        sub.padding = [paddingTop, paddingRight, paddingBottom, paddingLeft];
        
        return sub;
    }

    /**
     * 支持负值绘制,从右边计算
     * @param {*} item 
     * @param {*} style 
     */
    _resetPositionX (item, style) {
        let zoom = this.zoom;
        let x = 0;

        if(style.dataset && style.dataset.type) {
            zoom = 1;
        }

        // 通过wxml获取的不需要重置坐标
        if (item.x < 0 && item.type) {
            x = this.width + item.x * zoom - style.width * zoom;
        } else {
            x = item.x * zoom;
        }

        return x + this.translateX;
    }

    /**
     * 支持负值绘制,从底部计算
     * @param {*} item 
     * @param {*} style 
     */
    _resetPositionY (item, style, textHeight) {
        let zoom = this.zoom;
        let y = 0;

        if(style.dataset && style.dataset.type) {
            zoom = 1;
        }

        if (item.y < 0) {
            y = this.height + item.y * zoom - (textHeight ? textHeight : style.height * zoom)
        } else {
            y = item.y * zoom;
        }

        return y + this.translateY;
    }

    /**
     * 文字的padding、text-align
     * @param {*} item 
     * @param {*} style 
     * @param {*} textWidth
     */
    _resetTextPositionX (item, style, textWidth, width) {
        let textAlign = style.textAlign || 'left';
        let x = item.x;
        if (textAlign === 'center') {
            x = (width - textWidth) / 2 + item.x;
        } else if (textAlign === 'right') {
            x = width - textWidth + item.x;
        }

        let left = style.padding ? (style.padding[3] || 0) : 0;

        return x + left + this.translateX;
    }

    /**
     * 文字的padding、text-align
     * @param {*} item 
     * @param {*} style 
     * @param {*} textWidth
     */
    _resetTextPositionY (item, style, lineNum = 0) {
        let zoom = this.zoom;
        if(style.dataset && style.dataset.type) {
            zoom = 1;
        }

        let lineHeight = this._getLineHeight(style);
        let fontSize = Math.ceil((style.fontSize || 14) * zoom)

        let blockLineHeightFix = (style.dataset && style.dataset.type || '').indexOf('inline') > -1 ? 0 : (lineHeight - fontSize) / 2

        let top = style.padding ? (style.padding[0] || 0) : 0;

        // y + lineheight偏移 + 行数 + paddingTop + 整体画布位移
        return item.y + blockLineHeightFix + lineNum * lineHeight + top + this.translateY;
    }

    /**
     * 当文本超过宽度时,计算每一行应该绘制的文本
     * @param {*} text 
     * @param {*} width 
     * @param {*} singleLength 
     * @param {*} currentIndex 
     * @param {*} widthOffset
     */
    _getTextSingleLine(text, width, singleLength, currentIndex = 0, widthOffset = 0) {
        let offset = 0;
        let endIndex = currentIndex + singleLength + offset;
        let single = text.substring(currentIndex, endIndex);
        let singleWidth = this.measureWidth(single);

        while (Math.round(widthOffset + singleWidth) > width) {
            offset--;
            endIndex = currentIndex + singleLength + offset;
            single = text.substring(currentIndex, endIndex);
            singleWidth = this.measureWidth(single);
        }

        return {
            endIndex, 
            single, 
            singleWidth
        }
    }

    _drawBorder (border, style, callback) {
        let zoom = this.zoom;
        if(style.dataset && style.dataset.type) {
            zoom = 1;
        }
        border = Util.transferBorder(border);

        if (border && border.width) {
            // 空白阴影,清空掉边框的阴影
            this._drawBoxShadow();
            if (border) {
                
                this.ctx.setLineWidth(border.width * zoom);

                if (border.style === 'dashed') {
                    let dash = style.dash || [5, 5, 0];
                    let offset =  dash[2] || 0;
                    let array = [dash[0] || 5, dash[1] || 5];
                    this.ctx.setLineDash(array, offset);
                }
                this.ctx.setStrokeStyle(border.color);
            }
            callback && callback(border);
        }
    }  

    _drawBoxShadow (boxShadow, callback) {
        boxShadow = Util.transferBoxShadow(boxShadow);
        if (boxShadow) {
            this.ctx.setShadow(boxShadow.offsetX, boxShadow.offsetY, boxShadow.blur, boxShadow.color);
        }else {
            this.ctx.setShadow(0, 0, 0, '#ffffff');
        }

        callback && callback(boxShadow || {});
    }

    _setFill (fill, callback) {
        if(fill) {
            if (typeof fill === 'string') {
                this.ctx.setFillStyle(fill);
            } else {
                let line = fill.line;
                let color = fill.color;
                let grd = this.ctx.createLinearGradient(line[0], line[1], line[2], line[3]);
                grd.addColorStop(0, color[0]);
                grd.addColorStop(1, color[1]);
                this.ctx.setFillStyle(grd);
            }

            callback && callback();
        }
    }

    _setStroke (stroke, callback) {
        if(stroke) {
            if (typeof stroke === 'string') {
                this.ctx.setStrokeStyle(stroke);
            } else {
                let line = stroke.line;
                let color = stroke.color;
                let grd = this.ctx.createLinearGradient(line[0], line[1], line[2], line[3]);
                grd.addColorStop(0, color[0]);
                grd.addColorStop(1, color[1]);
                this.ctx.setStrokeStyle(grd);
            }

            callback && callback();
        }
    }
}

export default Wxml2Canvas;

util.js

/**
 * 获取字符的长度,full为true时,一个汉字算两个长度
 * @param {String} str 
 * @param {Boolean} full 
 */

function getTextLength (str, full) {
    let len = 0;
    for (let i = 0; i < str.length; i++) {
        let c = str.charCodeAt(i);
        //单字节加1 
        if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
            len++;
        }
        else {
            len += (full ? 2 : 1);
        }
    }
    return len;
}

/**
 * rgba(255, 255, 255, 1) => #ffffff
 * @param {String} color 
 */
function transferColor (color = '') {
    let res = '#';
    color = color.replace(/^rgba?\(/, '').replace(/\)$/, '');
    color = color.split(', ');
    
    color.length > 3 ? color.length = 3 : '';
    for(let item of color) {
        item = parseInt(item || 0);
        if(item < 10) {
            res += ('0' + item)
        }else {
            res += (item.toString(16))
        }
    }

    return res;
}

function transferBorder (border = '') {
    let res = border.match(/(\w+)px\s(\w+)\s(.*)/);
    let obj = {};

    if(res) {
         obj = {
            width: +res[1],
            style: res[2],
            color: res[3]
        }
    }
    
    return res ? obj : null;
}


/**
 * 内边距,依次为上右下左
 * @param {*} padding 
 */
function transferPadding (padding = '0 0 0 0') {
    padding = padding.split(' ');
    for(let i = 0, len = padding.length; i < len; i++) {
        padding[i] = +padding[i].replace('px', '');
    }

    return padding;
}
/**
 * type1: 0, 25, 17, rgba(0, 0, 0, 0.3)
 * type2: rgba(0, 0, 0, 0.3) 0px 25px 17px 0px => (0, 25, 17, rgba(0, 0, 0, 0.3))
 * @param {*} shadow 
 */
function transferBoxShadow(shadow = '', type) {
    if(!shadow || shadow === 'none') return;
    let color;
    let split;

    split = shadow.match(/(\w+)\s(\w+)\s(\w+)\s(rgb.*)/);

    if (split) {
        split.shift();
        shadow = split;
        color = split[3] || '#ffffff';
    } else {
        split = shadow.split(') ');
        color = split[0] + ')'
        shadow = split[1].split('px ');
    }

    return {
        offsetX: +shadow[0] || 0,
        offsetY: +shadow[1] || 0,
        blur: +shadow[2] || 0,
        color
    }
}

export default {
    getTextLength,
    transferBorder,
    transferColor,
    transferPadding,
    transferBoxShadow
}

以上代码请参考个人项目运行

你可能感兴趣的:(javascript,前端,html)