feat: init
This commit is contained in:
parent
30083154a8
commit
a4a7158ce9
5 changed files with 581 additions and 0 deletions
|
|
@ -71,5 +71,8 @@
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": "eslint --fix"
|
"*": "eslint --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kirklin/palette": "^0.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
|
|
@ -7,6 +7,10 @@ settings:
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@kirklin/palette':
|
||||||
|
specifier: ^0.0.0
|
||||||
|
version: 0.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@antfu/ni':
|
'@antfu/ni':
|
||||||
specifier: ^0.21.12
|
specifier: ^0.21.12
|
||||||
|
|
@ -1068,6 +1072,13 @@ packages:
|
||||||
- vitest
|
- vitest
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@kirklin/palette@0.0.0:
|
||||||
|
resolution: {integrity: sha512-dF6LsZAZNQ3F6+vWqVEBjdDrSX2Vs7jqsgHJ3xq/2L+oSd73wJ9JuTveNlHA/O/mgBhFS2ymuYtBfIeswssOxw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
dependencies:
|
||||||
|
colord: 2.9.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@nodelib/fs.scandir@2.1.5:
|
/@nodelib/fs.scandir@2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -2147,6 +2158,10 @@ packages:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/colord@2.9.3:
|
||||||
|
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/colorette@2.0.20:
|
/colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
||||||
206
src/constants.ts
Normal file
206
src/constants.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
export const DEFAULT_COLORS = {
|
||||||
|
red: [
|
||||||
|
"#fff2f0",
|
||||||
|
"#ffe9e6",
|
||||||
|
"#ffc3bd",
|
||||||
|
"#ff9b94",
|
||||||
|
"#ff706b",
|
||||||
|
"#f53f3f",
|
||||||
|
"#cf2b31",
|
||||||
|
"#a81b24",
|
||||||
|
"#820e1a",
|
||||||
|
"#5c0914",
|
||||||
|
],
|
||||||
|
orangered: [
|
||||||
|
"#fff7f0",
|
||||||
|
"#ffead9",
|
||||||
|
"#ffd1b0",
|
||||||
|
"#ffb587",
|
||||||
|
"#ff975e",
|
||||||
|
"#f77234",
|
||||||
|
"#d15321",
|
||||||
|
"#ab3913",
|
||||||
|
"#852308",
|
||||||
|
"#5e1505",
|
||||||
|
],
|
||||||
|
orange: [
|
||||||
|
"#fff6e6",
|
||||||
|
"#ffdca3",
|
||||||
|
"#ffc87a",
|
||||||
|
"#ffb152",
|
||||||
|
"#ff9729",
|
||||||
|
"#ff7d00",
|
||||||
|
"#d96200",
|
||||||
|
"#b34a00",
|
||||||
|
"#8c3600",
|
||||||
|
"#662400",
|
||||||
|
],
|
||||||
|
gold: [
|
||||||
|
"#fffdeb",
|
||||||
|
"#fff6c2",
|
||||||
|
"#ffec99",
|
||||||
|
"#ffe070",
|
||||||
|
"#ffd147",
|
||||||
|
"#f7ba1e",
|
||||||
|
"#d1940f",
|
||||||
|
"#ab7003",
|
||||||
|
"#855200",
|
||||||
|
"#5e3700",
|
||||||
|
],
|
||||||
|
yellow: [
|
||||||
|
"#feffe6",
|
||||||
|
"#ffffbd",
|
||||||
|
"#fffb94",
|
||||||
|
"#fff56b",
|
||||||
|
"#ffec42",
|
||||||
|
"#fadc19",
|
||||||
|
"#d4b20b",
|
||||||
|
"#ad8b00",
|
||||||
|
"#876800",
|
||||||
|
"#614700",
|
||||||
|
],
|
||||||
|
lime: [
|
||||||
|
"#fcffed",
|
||||||
|
"#f4ffc4",
|
||||||
|
"#e9ff9c",
|
||||||
|
"#d3f56e",
|
||||||
|
"#b9e843",
|
||||||
|
"#9fdb1d",
|
||||||
|
"#7bb50e",
|
||||||
|
"#5a8f04",
|
||||||
|
"#3d6900",
|
||||||
|
"#244200",
|
||||||
|
],
|
||||||
|
green: [
|
||||||
|
"#dcf5de",
|
||||||
|
"#95e89d",
|
||||||
|
"#69db78",
|
||||||
|
"#42cf5a",
|
||||||
|
"#1fc240",
|
||||||
|
"#00b42a",
|
||||||
|
"#008f26",
|
||||||
|
"#00691f",
|
||||||
|
"#004216",
|
||||||
|
"#001c0a",
|
||||||
|
],
|
||||||
|
cyan: [
|
||||||
|
"#e6fffb",
|
||||||
|
"#bbfcf4",
|
||||||
|
"#8bf0e6",
|
||||||
|
"#5fe3da",
|
||||||
|
"#38d6d1",
|
||||||
|
"#14c9c9",
|
||||||
|
"#089ea3",
|
||||||
|
"#00757d",
|
||||||
|
"#004e57",
|
||||||
|
"#002a30",
|
||||||
|
],
|
||||||
|
blue: [
|
||||||
|
"#f0f9ff",
|
||||||
|
"#d9f0ff",
|
||||||
|
"#b0ddff",
|
||||||
|
"#87c7ff",
|
||||||
|
"#5eafff",
|
||||||
|
"#3491fa",
|
||||||
|
"#226fd4",
|
||||||
|
"#1351ad",
|
||||||
|
"#083787",
|
||||||
|
"#052461",
|
||||||
|
],
|
||||||
|
kirklinBlue: [
|
||||||
|
"#e6f1ff",
|
||||||
|
"#bad8ff",
|
||||||
|
"#91bdff",
|
||||||
|
"#69a0ff",
|
||||||
|
"#407fff",
|
||||||
|
"#165dff",
|
||||||
|
"#0940d9",
|
||||||
|
"#002ab3",
|
||||||
|
"#001c8c",
|
||||||
|
"#001166",
|
||||||
|
],
|
||||||
|
purple: [
|
||||||
|
"#f9f0ff",
|
||||||
|
"#efdbff",
|
||||||
|
"#d3adf7",
|
||||||
|
"#b37feb",
|
||||||
|
"#9254de",
|
||||||
|
"#722ed1",
|
||||||
|
"#531dab",
|
||||||
|
"#391085",
|
||||||
|
"#22075e",
|
||||||
|
"#120338",
|
||||||
|
],
|
||||||
|
pinkpurple: [
|
||||||
|
"#ffebfc",
|
||||||
|
"#ffc2f7",
|
||||||
|
"#ff99f5",
|
||||||
|
"#f26be9",
|
||||||
|
"#e640e0",
|
||||||
|
"#d91ad9",
|
||||||
|
"#ad0cb3",
|
||||||
|
"#83038c",
|
||||||
|
"#5c0066",
|
||||||
|
"#370040",
|
||||||
|
],
|
||||||
|
magenta: [
|
||||||
|
"#fff0f6",
|
||||||
|
"#ffd6e7",
|
||||||
|
"#ffadd2",
|
||||||
|
"#ff85c0",
|
||||||
|
"#ff5cb0",
|
||||||
|
"#f5319d",
|
||||||
|
"#cf1f85",
|
||||||
|
"#a8116e",
|
||||||
|
"#820757",
|
||||||
|
"#5c0440",
|
||||||
|
],
|
||||||
|
gray: [
|
||||||
|
"#f7f8fa",
|
||||||
|
"#f2f3f5",
|
||||||
|
"#e5e6eb",
|
||||||
|
"#c9cdd4",
|
||||||
|
"#a9aeb8",
|
||||||
|
"#86909c",
|
||||||
|
"#6b7785",
|
||||||
|
"#4e5969",
|
||||||
|
"#272e3b",
|
||||||
|
"#1d2129",
|
||||||
|
],
|
||||||
|
grey: [
|
||||||
|
"#ced6db",
|
||||||
|
"#c2c9cf",
|
||||||
|
"#b6bdc2",
|
||||||
|
"#aab0b5",
|
||||||
|
"#9ea3a8",
|
||||||
|
"#86909c",
|
||||||
|
"#5f6875",
|
||||||
|
"#3c434f",
|
||||||
|
"#1d2129",
|
||||||
|
"#020203",
|
||||||
|
],
|
||||||
|
black: [
|
||||||
|
"#404040",
|
||||||
|
"#333333",
|
||||||
|
"#262626",
|
||||||
|
"#1a1a1a",
|
||||||
|
"#0d0d0d",
|
||||||
|
"#000000",
|
||||||
|
"#000000",
|
||||||
|
"#000000",
|
||||||
|
"#000000",
|
||||||
|
"#000000",
|
||||||
|
],
|
||||||
|
white: [
|
||||||
|
"#ffffff",
|
||||||
|
"#ffffff",
|
||||||
|
"#ffffff",
|
||||||
|
"#ffffff",
|
||||||
|
"#ffffff",
|
||||||
|
"#ffffff",
|
||||||
|
"#d9d9d9",
|
||||||
|
"#b3b3b3",
|
||||||
|
"#8c8c8c",
|
||||||
|
"#666666",
|
||||||
|
],
|
||||||
|
};
|
||||||
300
src/index.ts
300
src/index.ts
|
|
@ -0,0 +1,300 @@
|
||||||
|
import { DEFAULT_COLORS } from "./constants";
|
||||||
|
import type { Control, Wireframe } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a decimal color value to its RGB representation.
|
||||||
|
* @param color - The decimal color value.
|
||||||
|
* @returns The RGB representation of the color.
|
||||||
|
*/
|
||||||
|
function decimalToRGB(color: number): string {
|
||||||
|
const red = (color >> 16) & 255; // Extracting red channel
|
||||||
|
const green = (color >> 8) & 255; // Extracting green channel
|
||||||
|
const blue = color & 255; // Extracting blue channel
|
||||||
|
return `rgb(${red},${green},${blue})`; // Returning RGB representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 SVG 元素的辅助函数
|
||||||
|
function createSVGElement(tag: string, attributes: Record<string, any> = {}, parent?: SVGElement | HTMLElement): SVGElement {
|
||||||
|
const element: SVGElement = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||||
|
for (const attr in attributes) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
|
||||||
|
element.setAttribute(attr, attributes[attr]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent) {
|
||||||
|
parent.appendChild(element);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BORDER_WIDTH = 2.7;
|
||||||
|
const ARROW_WIDTH = 4;
|
||||||
|
const RECT_RADIUS = 10;
|
||||||
|
|
||||||
|
class Renderer {
|
||||||
|
private svgRoot: SVGElement;
|
||||||
|
private readonly fontFamily: string;
|
||||||
|
private canvasRenderingContext2D: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
constructor(svgRoot: SVGElement, fontFamily: string) {
|
||||||
|
this.svgRoot = svgRoot;
|
||||||
|
this.fontFamily = fontFamily;
|
||||||
|
this.canvasRenderingContext2D = document.createElement("canvas").getContext("2d")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(control: Control, container: any) {
|
||||||
|
const type = control.typeID;
|
||||||
|
if (type in this) {
|
||||||
|
// eslint-disable-next-line ts/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
this[type](control, container);
|
||||||
|
} else {
|
||||||
|
console.error(`'${type}' control type not implemented`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseColor(color: any, defaultColor: any): string {
|
||||||
|
return color === undefined ? `rgb(${defaultColor})` : decimalToRGB(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFontProperties(control: Control) {
|
||||||
|
const { properties } = control;
|
||||||
|
const style = properties && properties.italic ? "italic" : "normal";
|
||||||
|
const weight = properties && properties.bold ? "bold" : "normal";
|
||||||
|
const size = properties && properties.size ? `${properties.size}px` : "13px";
|
||||||
|
const family = this.fontFamily;
|
||||||
|
|
||||||
|
return { style, weight, size, family };
|
||||||
|
}
|
||||||
|
|
||||||
|
measureText(text: string, font: string): TextMetrics {
|
||||||
|
this.canvasRenderingContext2D.font = font;
|
||||||
|
return this.canvasRenderingContext2D.measureText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRectangle(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
const { x, y, w, measuredW, h, measuredH, properties } = control;
|
||||||
|
const rectX = Number.parseInt(x) + BORDER_WIDTH / 2;
|
||||||
|
const rectY = Number.parseInt(y) + BORDER_WIDTH / 2;
|
||||||
|
const rectWidth = Number.parseInt(w ?? measuredW) - BORDER_WIDTH;
|
||||||
|
const rectHeight = Number.parseInt(h ?? measuredH) - BORDER_WIDTH;
|
||||||
|
|
||||||
|
createSVGElement("rect", {
|
||||||
|
"x": rectX,
|
||||||
|
"y": rectY,
|
||||||
|
"width": rectWidth,
|
||||||
|
"height": rectHeight,
|
||||||
|
"rx": RECT_RADIUS,
|
||||||
|
"fill": this.parseColor(properties?.color, "255,255,255"),
|
||||||
|
"fill-opacity": properties?.backgroundAlpha ?? 1,
|
||||||
|
"stroke": this.parseColor(properties?.borderColor, "0,0,0"),
|
||||||
|
"stroke-width": BORDER_WIDTH,
|
||||||
|
}, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
addText(
|
||||||
|
control: Control,
|
||||||
|
container: HTMLElement | undefined,
|
||||||
|
textColor: string,
|
||||||
|
align: string,
|
||||||
|
): void {
|
||||||
|
const textContent = control.properties.text ?? "";
|
||||||
|
const xPosition = Number.parseInt(control.x);
|
||||||
|
const yPosition = Number.parseInt(control.y);
|
||||||
|
const fontProperties = this.parseFontProperties(control);
|
||||||
|
const textMetrics = this.measureText(
|
||||||
|
textContent,
|
||||||
|
`${fontProperties.style} ${fontProperties.weight} ${fontProperties.size} ${fontProperties.family}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const textX = align === "center" ? xPosition + (Number.parseInt(String(control.w)) ?? Number.parseInt(String(control.measuredW))) / 2 - textMetrics.width / 2 : xPosition;
|
||||||
|
const textY = yPosition + Number.parseInt(String(control.measuredH)) / 2 + textMetrics.actualBoundingBoxAscent / 2;
|
||||||
|
|
||||||
|
const textElement = createSVGElement("text", {
|
||||||
|
"x": textX,
|
||||||
|
"y": textY,
|
||||||
|
"fill": textColor,
|
||||||
|
"font-style": fontProperties.style,
|
||||||
|
"font-weight": fontProperties.weight,
|
||||||
|
"font-size": fontProperties.size,
|
||||||
|
}, container);
|
||||||
|
|
||||||
|
if (!textContent.includes("{color:")) {
|
||||||
|
const tspanElement = createSVGElement("tspan", {}, textElement);
|
||||||
|
tspanElement.textContent = textContent;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textContent.split(/{color:|{color}/).forEach((part) => {
|
||||||
|
if (part.includes("}")) {
|
||||||
|
let [colorCode, remainingText] = part.split("}");
|
||||||
|
if (!colorCode.startsWith("#")) {
|
||||||
|
const colorIndex = Number.parseInt(colorCode.slice(-1));
|
||||||
|
// eslint-disable-next-line ts/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
colorCode = Number.isNaN(colorIndex) ? DEFAULT_COLORS[colorCode][5] : DEFAULT_COLORS[colorCode][colorIndex];
|
||||||
|
}
|
||||||
|
const tspanElement = createSVGElement("tspan", { fill: colorCode }, textElement);
|
||||||
|
tspanElement.textContent = remainingText;
|
||||||
|
} else {
|
||||||
|
const tspanElement = createSVGElement("tspan", {}, textElement);
|
||||||
|
tspanElement.textContent = part;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TextArea(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
this.drawRectangle(control, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
Canvas(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
this.drawRectangle(control, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
Label(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
this.addText(
|
||||||
|
control,
|
||||||
|
container,
|
||||||
|
this.parseColor(control.properties?.color, "0,0,0"),
|
||||||
|
"left",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
this.drawRectangle(control, container);
|
||||||
|
this.addText(
|
||||||
|
control,
|
||||||
|
container,
|
||||||
|
this.parseColor(control.properties?.textColor, "0,0,0"),
|
||||||
|
"center",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrow(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
const { x, y, properties: { p0: startPoint, p1: controlPoint, p2: endPoint, stroke: lineStyle, color } } = control;
|
||||||
|
const strokeWidth = ARROW_WIDTH;
|
||||||
|
let dashArray = "";
|
||||||
|
|
||||||
|
if (lineStyle === "dotted") {
|
||||||
|
dashArray = "0.8 12";
|
||||||
|
} else if (lineStyle === "dashed") {
|
||||||
|
dashArray = "28 46";
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = { x: (endPoint.x - startPoint.x) * controlPoint.x, y: (endPoint.y - startPoint.y) * controlPoint.x };
|
||||||
|
|
||||||
|
createSVGElement("path", {
|
||||||
|
"d": `M${Number.parseInt(x) + startPoint.x} ${Number.parseInt(y) + startPoint.y}Q${Number.parseInt(x) + startPoint.x + l.x + l.y * controlPoint.y * 3.6} ${Number.parseInt(y) + startPoint.y + l.y + -l.x * controlPoint.y * 3.6} ${Number.parseInt(x) + endPoint.x} ${Number.parseInt(y) + endPoint.y}`,
|
||||||
|
"fill": "none",
|
||||||
|
"stroke": this.parseColor(color, "0,0,0"),
|
||||||
|
"stroke-width": strokeWidth,
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
"stroke-dasharray": dashArray,
|
||||||
|
}, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
const { x, y, properties: { color, icon: { ID } } } = control;
|
||||||
|
const circleRadius = 10;
|
||||||
|
|
||||||
|
createSVGElement("circle", {
|
||||||
|
cx: Number.parseInt(x) + circleRadius,
|
||||||
|
cy: Number.parseInt(y) + circleRadius,
|
||||||
|
r: circleRadius,
|
||||||
|
fill: this.parseColor(color, "0,0,0"),
|
||||||
|
}, container);
|
||||||
|
|
||||||
|
if (ID === "check-circle") {
|
||||||
|
createSVGElement("path", {
|
||||||
|
"d": `M${Number.parseInt(x) + 4.5} ${Number.parseInt(y) + circleRadius}L${Number.parseInt(x) + 8.5} ${Number.parseInt(y) + circleRadius + 4} ${Number.parseInt(x) + 15} ${Number.parseInt(y) + circleRadius - 2.5}`,
|
||||||
|
"fill": "none",
|
||||||
|
"stroke": "#fff",
|
||||||
|
"stroke-width": 3.5,
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
}, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRule(control: Control, container: HTMLElement | undefined): void {
|
||||||
|
const { x, y, properties: { stroke: lineStyle, color } } = control;
|
||||||
|
const strokeWidth = BORDER_WIDTH;
|
||||||
|
let dashArray = "";
|
||||||
|
|
||||||
|
if (lineStyle === "dotted") {
|
||||||
|
dashArray = "0.8, 8";
|
||||||
|
} else if (lineStyle === "dashed") {
|
||||||
|
dashArray = "18, 30";
|
||||||
|
}
|
||||||
|
|
||||||
|
createSVGElement("path", {
|
||||||
|
"d": `M${x} ${y}L${Number.parseInt(x) + Number.parseInt(control.w ?? control.measuredW)} ${y}`,
|
||||||
|
"fill": "none",
|
||||||
|
"stroke": this.parseColor(color, "0,0,0"),
|
||||||
|
"stroke-width": strokeWidth,
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
"stroke-dasharray": dashArray,
|
||||||
|
}, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
__group__(control: Control, container: any): void {
|
||||||
|
const groupName = control?.properties?.controlName || "";
|
||||||
|
const groupElement = createSVGElement("g", groupName
|
||||||
|
? {
|
||||||
|
"class": "clickable-group",
|
||||||
|
"data-group-id": groupName,
|
||||||
|
}
|
||||||
|
: {}, container);
|
||||||
|
|
||||||
|
control?.children?.controls.control.sort((a: any, b: any) => a.zOrder - b.zOrder).forEach((child: any) => {
|
||||||
|
child.x = Number.parseInt(child.x, 10) + Number.parseInt(control.x, 10);
|
||||||
|
child.y = Number.parseInt(child.y, 10) + Number.parseInt(control.y, 10);
|
||||||
|
this.render(child, groupElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function wireframeJSONToSVG(wireframe: Wireframe, options: { padding?: number; fontFamily?: string; fontURL?: string } = {}): Promise<SVGElement> {
|
||||||
|
options = {
|
||||||
|
padding: 5,
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
fontURL: "",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.fontURL) {
|
||||||
|
const font = new FontFace(options.fontFamily!, `url(${options.fontURL})`);
|
||||||
|
await font.load();
|
||||||
|
// eslint-disable-next-line ts/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
if (document.fonts.add) {
|
||||||
|
// eslint-disable-next-line ts/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
document.fonts.add(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockup = wireframe.mockup;
|
||||||
|
const padding = options.padding!;
|
||||||
|
const paddingRight = Number.parseInt(String(mockup.measuredW)) - Number.parseInt(String(mockup.mockupW)) - padding;
|
||||||
|
const paddingBottom = Number.parseInt(String(mockup.measuredH)) - Number.parseInt(String(mockup.mockupH)) - padding;
|
||||||
|
const svgWidth = Number.parseInt(String(mockup.mockupW)) + padding * 2;
|
||||||
|
const svgHeight = Number.parseInt(String(mockup.mockupH)) + padding * 2;
|
||||||
|
|
||||||
|
const svgElement = createSVGElement("svg", {
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg",
|
||||||
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||||
|
"viewBox": `${paddingRight} ${paddingBottom} ${svgWidth} ${svgHeight}`,
|
||||||
|
"style": `font-family: ${options.fontFamily}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderer = new Renderer(svgElement, options.fontFamily!);
|
||||||
|
|
||||||
|
mockup.controls.control.sort((a: any, b: any) => a.zOrder - b.zOrder).forEach((control: Control) => {
|
||||||
|
renderer.render(control, svgElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
return svgElement;
|
||||||
|
}
|
||||||
57
src/types.ts
Normal file
57
src/types.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
export interface Wireframe {
|
||||||
|
mockup: {
|
||||||
|
measuredW: string; // 界面的测量宽度
|
||||||
|
measuredH: string; // 界面的测量高度
|
||||||
|
mockupW: string; // 界面的宽度
|
||||||
|
mockupH: string; // 界面的高度
|
||||||
|
controls: {
|
||||||
|
control: Control[]; // 控件数组
|
||||||
|
};
|
||||||
|
// 其他属性...
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
name: string; // 名称
|
||||||
|
order: number; // 排序顺序
|
||||||
|
parentID: string | null; // 父级ID(可能为 null)
|
||||||
|
notes: string; // 备注
|
||||||
|
// 其他属性...
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
branchID: string; // 分支ID
|
||||||
|
resourceID: string; // 资源ID
|
||||||
|
version: string; // 版本号
|
||||||
|
groupOffset: {
|
||||||
|
x: number | string; // 组偏移的X坐标
|
||||||
|
y: number | string; // 组偏移的Y坐标
|
||||||
|
};
|
||||||
|
dependencies: any[]; // 依赖项(可能为空数组)
|
||||||
|
projectID: string; // 项目ID
|
||||||
|
// 其他属性...
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
export interface Control {
|
||||||
|
ID: string; // 控件的唯一标识符
|
||||||
|
typeID: string; // 控件类型的标识
|
||||||
|
zOrder: string; // 控件的层级顺序
|
||||||
|
measuredW: string; // 控件的测量宽度
|
||||||
|
measuredH: string; // 控件的测量高度
|
||||||
|
w: string; // 控件的宽度
|
||||||
|
h: string; // 控件的高度
|
||||||
|
x: string; // 控件的 X 坐标
|
||||||
|
y: string; // 控件的 Y 坐标
|
||||||
|
properties: {
|
||||||
|
controlName?: string; // 控件名称(可能存在,可能为空)
|
||||||
|
size?: string; // 控件的文字大小(可能存在,可能为空)
|
||||||
|
text?: string; // 控件的文字内容(可能存在,可能为空)
|
||||||
|
// 其他属性...
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
children?: {
|
||||||
|
controls: {
|
||||||
|
control: Control[]; // 子控件数组
|
||||||
|
};
|
||||||
|
// 其他属性...
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue