初始化代码

This commit is contained in:
2025-12-22 17:13:05 +08:00
parent ed0de08e3a
commit 1f7e9d401b
2947 changed files with 526137 additions and 0 deletions

View 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
};

View File

@@ -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([])
})])
});
});

View File

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

View File

@@ -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
};

View File

@@ -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
};

View File

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