UNPKG

crazy-poster-taro

Version:

a wxml2canvas tool for taro.

464 lines (428 loc) 15 kB
import Taro from "@tarojs/taro"; import cloneDeep from "lodash.clonedeep"; export default class JzWxml2Canvas { constructor(options = {}) { this.drawClass = options.draw this.limit = options.limit this.canvas = options.canvas this.finish = options.finish this.fail = options.fail this.scope = options.scope try { this._init() } catch (error) { const errorObj = { msg: "初始化失败", error } this.fail(errorObj) throw new Error("初始化失败") } } draw() { this._draw() } _init() { this.ctx = Taro.createCanvasContext(this.canvas); this._wxmlInitial() } _wxmlInitial() { try { this._getWxml() .then(element => { const fatherOrigin = { x: element[1].left, y: element[1].top } const regex = /url\("([^"]+)"\)/; const match = element[1].backgroundImage.match(regex); const arr = element[0].map((item, index, array) => { item.left = item.left - fatherOrigin.x item.top = item.top - fatherOrigin.y item._width = parseInt(item.width) item._height = parseInt(item.height) if (parseInt(item.borderWidth)) { item.left = item.left + parseInt(item.borderWidth) item.top = item.top + parseInt(item.borderWidth) item._height = item._height + parseInt(item.borderWidth) item._width = item._width + parseInt(item.borderWidth) } if (item.dataset && item.dataset.type === "text-struct" && array[index + 1].dataset.text) { array[index + 1].maxWidth = item._width } return item }) element[1]["_width"] = parseInt(element[1].width) element[1]["_height"] = parseInt(element[1].height) const cloneElement1 = { ...cloneDeep(element[1]), dataset: { url: match && match[1] }, full: true } arr.unshift(cloneElement1) this.allDraw = arr this.father = element[1] this._preloadImage(arr).catch(err => { const errorObj = { msg: "图片预加载失败", error: err } this.fail(errorObj) throw new Error("图片预加载失败") }) }) .catch(err => { const errorObj = { msg: "获取wxml元素失败", error: err } this.fail(errorObj) throw new Error("获取wxml元素失败") }) } catch (error) { const errorObj = { msg: "wxml元素初始化失败", error } this.fail(errorObj) throw new Error("wxml元素初始化失败") } } _draw() { console.log('this.allDraw', this.allDraw); try { this.allDraw.map(element => { if (element.dataset && element.dataset.url) { this._drawImage(element, element.full) }else if(element.dataset && element.dataset.type === "line") { this._drawBorder(element) } else if (element.dataset && element.dataset.type === "rect") { this._drawRectangle(element) } else if (element.dataset && element.dataset.text) { this._drawText(element) } }) } catch (error) { const errorObj = { msg: "绘制过程失败", error } this.fail(errorObj) throw new Error("绘制过程失败") } try { this.ctx.draw(true, () => { Taro.canvasToTempFilePath({ x: 0, y: 0, canvasId: this.canvas, success: res => { const { tempFilePath } = res this.finish(tempFilePath) }, fail: err => { const errorObj = { msg: "canvas转图片失败", error: err } this.fail(errorObj) throw new Error("canvas转图片失败") } }) }) } catch (error) { const errorObj = { msg: "绘制失败", error } this.fail(errorObj) throw new Error("绘制失败") } } _getWxml() { const promise_all = new Promise((resolve, reject) => { Taro.createSelectorQuery().in(this.scope).selectAll(this.drawClass).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', 'borderRadius', 'borderBottom' ] }, res => { resolve(res) }).exec() }) const promise_limit = new Promise((resolve, reject) => { Taro.createSelectorQuery().in(this.scope).select(this.limit).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 => { resolve(res) }).exec(); }) return Promise.all([promise_all, promise_limit]); } _preloadImage(arr) { const self = this const promiseArr = [] this.allDraw = arr.map(item => { if (item.dataset && item.dataset.url && !item.dataset.url.includes('skip')) { const { url } = item.dataset promiseArr.push(this.downloadImage_tool(url, (url) => { item.url = url })) } return item }) return Promise.all(promiseArr) } _drawBoldText(text, x, y) { const left = x, top = y; const distance = 0.01; this.ctx.fillText(text, left, top) // this.ctx.fillText(text, left, top - distance) // this.ctx.fillText(text, left - distance, top) // this.ctx.fillText(text, left, top) // this.ctx.fillText(text, left, top + distance) // this.ctx.fillText(text, left + distance, top) } _drawText(element) { const { dataset, fontWeight, fontFamily, fontSize, color, left, top, maxWidth = 0, lineHeight } = element const _fontWeight = parseInt(fontWeight) const _fontSize = parseInt(fontSize) const text = dataset.text; let y = top + _fontSize; const whichDrawText = (text = "", x = 0, y = 0) => { if (_fontWeight >= 500) { this._drawBoldText(text, x, y) } else { this.ctx.fillText(text, x, y); } } this.ctx.save() this.ctx.font = `${_fontWeight} ${_fontSize}px ${fontFamily}`; this.ctx.setFillStyle(color); // 文本换行绘制 if (maxWidth) { let line = ''; const _lineHeight = parseInt(lineHeight) const result = []; for (let i = 0; i < text.length; i++) { const testLine = line + text[i]; const metrics = this.ctx.measureText(testLine); const testWidth = metrics.width; if (testWidth > maxWidth && line.length > 0) { result.push(line); line = text[i]; } else { line = testLine; } } result.push(line); result.map(line => { whichDrawText(line, left, y) y += (_fontSize + (_lineHeight / 2)) }) } else { whichDrawText(text, left, y) } this.ctx.restore(); } _drawImage(element, full=false) { const { url, left, top, _width, _height, borderRadius = 0 } = element const radius = parseInt(borderRadius) const x = full ? 0:left const y = full ? 0:top this.ctx.save(); if (radius) { // 绘制圆角矩形路径 this.ctx.beginPath(); this.ctx.moveTo(left + radius, top); this.ctx.lineTo(left + _width - radius, top); this.ctx.quadraticCurveTo(left + _width, top, left + _width, top + radius); this.ctx.lineTo(left + _width, top + _height - radius); this.ctx.quadraticCurveTo(left + _width, top + _height, left + _width - radius, top + _height); this.ctx.lineTo(left + radius, top + _height); this.ctx.quadraticCurveTo(left, top + _height, left, top + _height - radius); this.ctx.lineTo(left, top + radius); this.ctx.quadraticCurveTo(left, top, left + radius, top); this.ctx.closePath(); this.ctx.clip(); } this.ctx.drawImage(url, x, y, _width, _height) this.ctx.restore(); } _drawRectangle(element) { const { backgroundImage, borderWidth } = element if (borderWidth.split(" ").length > 1 || (borderWidth.split(" ").length === 1 && parseInt(borderWidth))) { this._drawBorder(element) } if (backgroundImage.includes('linear-gradient')) { this._drawLinearGradient(element) } else { this._drawBackground(element) } } _drawBorder(element) { const { borderColor, borderWidth, left, top, _width, _height } = element; const borderWidths = borderWidth.split(' ').map(value => Math.round(parseFloat(value) || 0)); const borderColors = borderColor.split(') ').map((i, index, arr) => index + 1 !== arr.length ? i + ")" : i); this.ctx.save(); if (borderWidths.length === 1) { this.ctx.setStrokeStyle(borderColor); this.ctx.setLineWidth(parseInt(borderWidth, 10)); } else { const topBorder = borderWidths[0]; const leftRightBorder = borderWidths[1]; const bottomBorder = borderWidths[2]; const topColor = borderColors[0]; const leftRightColor = borderColors[1]; const bottomColor = borderColors[2]; if (topBorder) { this.ctx.beginPath(); this.ctx.setLineWidth(topBorder); this.ctx.setStrokeStyle(topColor); this.ctx.moveTo(left, top - 0.5); this.ctx.lineTo(left + _width, top - 0.5); this.ctx.stroke(); } if (leftRightBorder) { this.ctx.beginPath(); this.ctx.setLineWidth(leftRightBorder); this.ctx.setStrokeStyle(leftRightColor); this.ctx.moveTo(left, top - 0.5); this.ctx.lineTo(left, top + _height - 0.5); this.ctx.stroke(); } if (leftRightBorder) { this.ctx.beginPath(); this.ctx.setLineWidth(leftRightBorder); this.ctx.setStrokeStyle(leftRightColor); this.ctx.moveTo(left + _width, top - 0.5); this.ctx.lineTo(left + _width, top + _height - 0.5); this.ctx.stroke(); } if (bottomBorder) { this.ctx.beginPath(); this.ctx.setLineWidth(bottomBorder); this.ctx.setStrokeStyle(bottomColor); this.ctx.moveTo(left + _width, top + _height - 0.5); this.ctx.lineTo(left, top + _height - 0.5); this.ctx.stroke(); } } this.ctx.restore(); } _drawLinearGradient(element, isFull = false) { const { backgroundImage, left, top, _width, _height, borderRadius } = element const matchArr = this.matchAllRgb(backgroundImage) const isToBottom = backgroundImage.includes("540deg") const _left = isFull ? 0 : left const _top = isFull ? 0 : top const orientation = () => { if (isToBottom) return [left, top, left, top + _height] return [left, top, left + _width, top] } const grd = this.ctx.createLinearGradient(...orientation()) const radius = borderRadius ? parseInt(borderRadius) : 0 this.ctx.save() grd.addColorStop(0, matchArr[0]) grd.addColorStop(1, matchArr[1]) this.ctx.setFillStyle(grd) if (radius) { this._drawRadius(element) } else { this.ctx.fillRect(_left, _top, _width, _height) } this.ctx.restore() } _drawBackground(element, isFull = false) { const { left, top, _width, _height, backgroundColor, borderRadius } = element const _left = isFull ? 0 : left const _top = isFull ? 0 : top const radius = borderRadius ? parseInt(borderRadius) : 0 this.ctx.save(); this.ctx.setFillStyle(backgroundColor) if (radius) { this._drawRadius(element) } else { this.ctx.fillRect(_left, _top, _width, _height) } this.ctx.restore(); } _drawRadius(element) { const { left, top, _width, _height, borderRadius } = element; const differentCornerComputed = () => { const radius = borderRadius.split(' ').map(value => parseInt(value) || 0); switch (radius.length) { case 1: return [radius[0], radius[0], radius[0], radius[0]]; case 2: return [radius[0], radius[1], radius[0], radius[1]]; case 3: return [radius[0], radius[1], radius[2], radius[1]]; case 4: return radius; default: return [0, 0, 0, 0]; } } const radius = differentCornerComputed() this.ctx.save(); this.ctx.beginPath(); // 绘制带有不同圆角的矩形 this.ctx.moveTo(left + radius[0], top); this.ctx.lineTo(left + _width - radius[1], top); this.ctx.quadraticCurveTo(left + _width, top, left + _width, top + radius[1]); this.ctx.lineTo(left + _width, top + _height - radius[2]); this.ctx.quadraticCurveTo(left + _width, top + _height, left + _width - radius[2], top + _height); this.ctx.lineTo(left + radius[3], top + _height); this.ctx.quadraticCurveTo(left, top + _height, left, top + _height - radius[3]); this.ctx.lineTo(left, top + radius[0]); this.ctx.quadraticCurveTo(left, top, left + radius[0], top); this.ctx.fill(); this.ctx.closePath(); this.ctx.restore(); } /** * 下载图片 * @param {string} url - 在线图片地址 * @param {Function} callback - 下载成功后的回调函数 */ downloadImage_tool(url, callback) { return new Promise((resolve, reject) => { Taro.downloadFile({ url: url.replace('http://', 'https://'), success: res => { const { statusCode, tempFilePath } = res const isFilePathOK = !tempFilePath.includes('.json') if (statusCode === 200 && isFilePathOK) { callback(tempFilePath) resolve(tempFilePath) } else { reject(errMsg) } }, fail: () => { reject(errMsg) } }) }) } /** * 匹配字符串中所有 rgb(***) 的内容 * @param {string} str - 输入的字符串 * @returns {Array<string>} - 匹配到的 rgb(***) 内容数组 */ matchAllRgb(str, regexStr = "rgb") { // 使用正则表达式匹配所有 rgb(***) 的内容 const regex = new RegExp(`${regexStr}a?\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*(?:,\\s*\\d*\\.?\\d+\\s*)?\\)`, 'g'); const matches = str.match(regex); return matches || []; } }