初始化代码
This commit is contained in:
219
uniapp/uni-app/components/miniprogram_npm/wxml-to-canvas/draw.js
Normal file
219
uniapp/uni-app/components/miniprogram_npm/wxml-to-canvas/draw.js
Normal file
@@ -0,0 +1,219 @@
|
||||
class Draw {
|
||||
constructor(canvas, context) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = context;
|
||||
}
|
||||
|
||||
roundRect(x, y, w, h, r, fill = true, stroke = false) {
|
||||
if (r < 0) return;
|
||||
const ctx = this.ctx;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2);
|
||||
ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0);
|
||||
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2);
|
||||
ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI);
|
||||
ctx.lineTo(x, y + r);
|
||||
if (stroke) ctx.stroke();
|
||||
if (fill) ctx.fill();
|
||||
}
|
||||
|
||||
drawView(box, style) {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
left: x,
|
||||
top: y,
|
||||
width: w,
|
||||
height: h
|
||||
} = box;
|
||||
const {
|
||||
borderRadius = 0,
|
||||
borderWidth = 0,
|
||||
borderColor,
|
||||
color = '#000',
|
||||
backgroundColor = 'transparent'
|
||||
} = style;
|
||||
ctx.save(); // 外环
|
||||
|
||||
if (borderWidth > 0) {
|
||||
ctx.fillStyle = borderColor || color;
|
||||
this.roundRect(x, y, w, h, borderRadius);
|
||||
} // 内环
|
||||
|
||||
|
||||
ctx.fillStyle = backgroundColor;
|
||||
const innerWidth = w - 2 * borderWidth;
|
||||
const innerHeight = h - 2 * borderWidth;
|
||||
const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0;
|
||||
this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
async drawImage(img, box, style) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const ctx = this.ctx;
|
||||
const canvas = this.canvas;
|
||||
const {
|
||||
borderRadius = 0
|
||||
} = style;
|
||||
const {
|
||||
left: x,
|
||||
top: y,
|
||||
width: w,
|
||||
height: h
|
||||
} = box;
|
||||
ctx.save();
|
||||
this.roundRect(x, y, w, h, borderRadius, false, false);
|
||||
ctx.clip();
|
||||
const Image = canvas.createImage();
|
||||
|
||||
Image.onload = () => {
|
||||
ctx.drawImage(Image, x, y, w, h);
|
||||
ctx.restore();
|
||||
resolve();
|
||||
};
|
||||
|
||||
Image.onerror = () => {
|
||||
reject();
|
||||
};
|
||||
|
||||
Image.src = img;
|
||||
});
|
||||
} // eslint-disable-next-line complexity
|
||||
|
||||
|
||||
drawText(text, box, style) {
|
||||
const ctx = this.ctx;
|
||||
let {
|
||||
left: x,
|
||||
top: y,
|
||||
width: w,
|
||||
height: h
|
||||
} = box;
|
||||
let {
|
||||
color = '#000',
|
||||
lineHeight = '1.4em',
|
||||
fontSize = 14,
|
||||
textAlign = 'left',
|
||||
verticalAlign = 'top',
|
||||
backgroundColor = 'transparent'
|
||||
} = style;
|
||||
if (!text || lineHeight > h) return;
|
||||
ctx.save();
|
||||
|
||||
if (lineHeight) {
|
||||
// 2em
|
||||
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize);
|
||||
}
|
||||
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.font = `${fontSize}px sans-serif`;
|
||||
ctx.textAlign = textAlign; // 背景色
|
||||
|
||||
ctx.fillStyle = backgroundColor;
|
||||
this.roundRect(x, y, w, h, 0); // 文字颜色
|
||||
|
||||
ctx.fillStyle = color; // 水平布局
|
||||
|
||||
switch (textAlign) {
|
||||
case 'left':
|
||||
break;
|
||||
|
||||
case 'center':
|
||||
x += 0.5 * w;
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
x += w;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const textWidth = ctx.measureText(text).width;
|
||||
const actualHeight = Math.ceil(textWidth / w) * lineHeight;
|
||||
let paddingTop = Math.ceil((h - actualHeight) / 2);
|
||||
if (paddingTop < 0) paddingTop = 0; // 垂直布局
|
||||
|
||||
switch (verticalAlign) {
|
||||
case 'top':
|
||||
break;
|
||||
|
||||
case 'middle':
|
||||
y += paddingTop;
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
y += 2 * paddingTop;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2); // 不超过一行
|
||||
|
||||
if (textWidth <= w) {
|
||||
ctx.fillText(text, x, y + inlinePaddingTop);
|
||||
return;
|
||||
} // 多行文本
|
||||
|
||||
|
||||
const chars = text.split('');
|
||||
const _y = y; // 逐行绘制
|
||||
|
||||
let line = '';
|
||||
|
||||
for (const ch of chars) {
|
||||
const testLine = line + ch;
|
||||
const testWidth = ctx.measureText(testLine).width;
|
||||
|
||||
if (testWidth > w) {
|
||||
ctx.fillText(line, x, y + inlinePaddingTop);
|
||||
y += lineHeight;
|
||||
line = ch;
|
||||
if (y + lineHeight > _y + h) break;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
} // 避免溢出
|
||||
|
||||
|
||||
if (y + lineHeight <= _y + h) {
|
||||
ctx.fillText(line, x, y + inlinePaddingTop);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
async drawNode(element) {
|
||||
const {
|
||||
layoutBox,
|
||||
computedStyle,
|
||||
name
|
||||
} = element;
|
||||
const {
|
||||
src,
|
||||
text
|
||||
} = element.attributes;
|
||||
|
||||
if (name === 'view') {
|
||||
this.drawView(layoutBox, computedStyle);
|
||||
} else if (name === 'image') {
|
||||
await this.drawImage(src, layoutBox, computedStyle);
|
||||
} else if (name === 'text') {
|
||||
this.drawText(text, layoutBox, computedStyle);
|
||||
}
|
||||
|
||||
const childs = Object.values(element.children);
|
||||
|
||||
for (const child of childs) {
|
||||
await this.drawNode(child);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Draw
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const $filterNullChildren = children => {
|
||||
children = deepFlatten(children);
|
||||
return children.filter(child => child != null);
|
||||
};
|
||||
|
||||
const deepFlatten = arr => {
|
||||
let flatten = arr => [].concat(...arr);
|
||||
|
||||
return flatten(arr.map(x => Array.isArray(x) ? deepFlatten(x) : x));
|
||||
};
|
||||
|
||||
export default ((data, opt) => {
|
||||
const {
|
||||
View,
|
||||
Text,
|
||||
Image
|
||||
} = opt;
|
||||
Object.assign(data, {});
|
||||
return new View({
|
||||
style: {},
|
||||
attr: {
|
||||
"needRoot": true
|
||||
},
|
||||
children: $filterNullChildren([new Canvas({
|
||||
style: {},
|
||||
attr: {
|
||||
"id": "canvas",
|
||||
"type": "2d",
|
||||
"style": "width: " + data.width + "px; height: " + data.height + "px;"
|
||||
},
|
||||
children: $filterNullChildren([])
|
||||
})])
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view>
|
||||
<canvas id="canvas" type="2d" :style="'width: ' + width + 'px; height: ' + height + 'px;'"></canvas>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const xmlParse = require("./xml-parser");
|
||||
const {
|
||||
Widget
|
||||
} = require("./widget");
|
||||
const {
|
||||
Draw
|
||||
} = require("./draw");
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
components: {},
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 300
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
const dpr = wx.getSystemInfoSync().pixelRatio;
|
||||
const query = this.createSelectorQuery();
|
||||
this.dpr = dpr;
|
||||
query.select('#canvas').fields({
|
||||
node: true,
|
||||
size: true
|
||||
}).exec(res => {
|
||||
console.log(res, "==res");
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = res[0].width * dpr;
|
||||
canvas.height = res[0].height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
this.ctx = ctx;
|
||||
this.canvas = canvas;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
async renderToCanvas(args) {
|
||||
const {
|
||||
wxml,
|
||||
style
|
||||
} = args; // 清空画布
|
||||
|
||||
const ctx = this.ctx;
|
||||
const canvas = this.canvas;
|
||||
|
||||
if (!ctx || !canvas) {
|
||||
return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'));
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, this.width, this.height);
|
||||
const {
|
||||
root: xom
|
||||
} = xmlParse(wxml);
|
||||
const widget = new Widget(xom, style);
|
||||
const container = widget.init();
|
||||
this.boundary = {
|
||||
top: container.layoutBox.top,
|
||||
left: container.layoutBox.left,
|
||||
width: container.computedStyle.width,
|
||||
height: container.computedStyle.height
|
||||
};
|
||||
const draw = new Draw(canvas, ctx);
|
||||
await draw.drawNode(container);
|
||||
return Promise.resolve(container);
|
||||
},
|
||||
|
||||
canvasToTempFilePath(args = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {
|
||||
top,
|
||||
left,
|
||||
width,
|
||||
height
|
||||
} = this.boundary;
|
||||
wx.canvasToTempFilePath({
|
||||
x: left,
|
||||
y: top,
|
||||
width,
|
||||
height,
|
||||
destWidth: width * this.dpr,
|
||||
destHeight: height * this.dpr,
|
||||
canvas: this.canvas,
|
||||
fileType: args.fileType || 'png',
|
||||
quality: args.quality || 1,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
const hex = color => {
|
||||
let result = null;
|
||||
|
||||
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
|
||||
return color; // eslint-disable-next-line no-cond-assign
|
||||
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
|
||||
return '#' + result[2].split(',').map((part, index) => {
|
||||
part = part.trim();
|
||||
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10);
|
||||
part = part.toString(16);
|
||||
|
||||
if (part.length === 1) {
|
||||
part = '0' + part;
|
||||
}
|
||||
|
||||
return part;
|
||||
}).join('');
|
||||
} else {
|
||||
return '#00000000';
|
||||
}
|
||||
};
|
||||
|
||||
const splitLineToCamelCase = str => str.split('-').map((part, index) => {
|
||||
if (index === 0) {
|
||||
return part;
|
||||
}
|
||||
|
||||
return part[0].toUpperCase() + part.slice(1);
|
||||
}).join('');
|
||||
|
||||
module.exports = {
|
||||
hex,
|
||||
splitLineToCamelCase
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
const Block = require("@/components/miniprogram_npm/widget-ui/index.js");
|
||||
|
||||
const {
|
||||
splitLineToCamelCase
|
||||
} = require("./utils.js");
|
||||
|
||||
class Element extends Block {
|
||||
constructor(prop) {
|
||||
super(prop.style);
|
||||
this.name = prop.name;
|
||||
this.attributes = prop.attributes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Widget {
|
||||
constructor(xom, style) {
|
||||
this.xom = xom;
|
||||
this.style = style;
|
||||
this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color'];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container = this.create(this.xom);
|
||||
this.container.layout();
|
||||
this.inheritStyle(this.container);
|
||||
return this.container;
|
||||
} // 继承父节点的样式
|
||||
|
||||
|
||||
inheritStyle(node) {
|
||||
const parent = node.parent || null;
|
||||
const children = node.children || {};
|
||||
const computedStyle = node.computedStyle;
|
||||
|
||||
if (parent) {
|
||||
this.inheritProps.forEach(prop => {
|
||||
computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop];
|
||||
});
|
||||
}
|
||||
|
||||
Object.values(children).forEach(child => {
|
||||
this.inheritStyle(child);
|
||||
});
|
||||
}
|
||||
|
||||
create(node) {
|
||||
let classNames = (node.attributes.class || '').split(' ');
|
||||
classNames = classNames.map(item => splitLineToCamelCase(item.trim()));
|
||||
const style = {};
|
||||
classNames.forEach(item => {
|
||||
Object.assign(style, this.style[item] || {});
|
||||
});
|
||||
const args = {
|
||||
name: node.name,
|
||||
style
|
||||
};
|
||||
const attrs = Object.keys(node.attributes);
|
||||
const attributes = {};
|
||||
|
||||
for (const attr of attrs) {
|
||||
const value = node.attributes[attr];
|
||||
const CamelAttr = splitLineToCamelCase(attr);
|
||||
|
||||
if (value === '' || value === 'true') {
|
||||
attributes[CamelAttr] = true;
|
||||
} else if (value === 'false') {
|
||||
attributes[CamelAttr] = false;
|
||||
} else {
|
||||
attributes[CamelAttr] = value;
|
||||
}
|
||||
}
|
||||
|
||||
attributes.text = node.content;
|
||||
args.attributes = attributes;
|
||||
const element = new Element(args);
|
||||
node.children.forEach(childNode => {
|
||||
const childElement = this.create(childNode);
|
||||
element.add(childElement);
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Widget
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Expose `parse`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse the given string of `xml`.
|
||||
*
|
||||
* @param {String} xml
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
function parse(xml) {
|
||||
xml = xml.trim(); // strip comments
|
||||
|
||||
xml = xml.replace(/<!--[\s\S]*?-->/g, '');
|
||||
return document();
|
||||
/**
|
||||
* XML document.
|
||||
*/
|
||||
|
||||
function document() {
|
||||
return {
|
||||
declaration: declaration(),
|
||||
root: tag()
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Declaration.
|
||||
*/
|
||||
|
||||
|
||||
function declaration() {
|
||||
const m = match(/^<\?xml\s*/);
|
||||
if (!m) return; // tag
|
||||
|
||||
const node = {
|
||||
attributes: {}
|
||||
}; // attributes
|
||||
|
||||
while (!(eos() || is('?>'))) {
|
||||
const attr = attribute();
|
||||
if (!attr) return node;
|
||||
node.attributes[attr.name] = attr.value;
|
||||
}
|
||||
|
||||
match(/\?>\s*/);
|
||||
return node;
|
||||
}
|
||||
/**
|
||||
* Tag.
|
||||
*/
|
||||
|
||||
|
||||
function tag() {
|
||||
const m = match(/^<([\w-:.]+)\s*/);
|
||||
if (!m) return; // name
|
||||
|
||||
const node = {
|
||||
name: m[1],
|
||||
attributes: {},
|
||||
children: []
|
||||
}; // attributes
|
||||
|
||||
while (!(eos() || is('>') || is('?>') || is('/>'))) {
|
||||
const attr = attribute();
|
||||
if (!attr) return node;
|
||||
node.attributes[attr.name] = attr.value;
|
||||
} // self closing tag
|
||||
|
||||
|
||||
if (match(/^\s*\/>\s*/)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
match(/\??>\s*/); // content
|
||||
|
||||
node.content = content(); // children
|
||||
|
||||
let child;
|
||||
|
||||
while (child = tag()) {
|
||||
node.children.push(child);
|
||||
} // closing
|
||||
|
||||
|
||||
match(/^<\/[\w-:.]+>\s*/);
|
||||
return node;
|
||||
}
|
||||
/**
|
||||
* Text content.
|
||||
*/
|
||||
|
||||
|
||||
function content() {
|
||||
const m = match(/^([^<]*)/);
|
||||
if (m) return m[1];
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Attribute.
|
||||
*/
|
||||
|
||||
|
||||
function attribute() {
|
||||
const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/);
|
||||
if (!m) return;
|
||||
return {
|
||||
name: m[1],
|
||||
value: strip(m[2])
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Strip quotes from `val`.
|
||||
*/
|
||||
|
||||
|
||||
function strip(val) {
|
||||
return val.replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
/**
|
||||
* Match `re` and advance the string.
|
||||
*/
|
||||
|
||||
|
||||
function match(re) {
|
||||
const m = xml.match(re);
|
||||
if (!m) return;
|
||||
xml = xml.slice(m[0].length);
|
||||
return m;
|
||||
}
|
||||
/**
|
||||
* End-of-source.
|
||||
*/
|
||||
|
||||
|
||||
function eos() {
|
||||
return xml.length == 0;
|
||||
}
|
||||
/**
|
||||
* Check for `prefix`.
|
||||
*/
|
||||
|
||||
|
||||
function is(prefix) {
|
||||
return xml.indexOf(prefix) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = parse;
|
||||
Reference in New Issue
Block a user