crazy-poster-taro
Version:
a wxml2canvas tool for taro.
464 lines (428 loc) • 15 kB
JavaScript
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 || [];
}
}