declare-render 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/arrow-test.ts +252 -0
- package/examples/output/arrow-test.png +0 -0
- package/examples/output/shape-test.png +0 -0
- package/examples/shape-test.ts +411 -0
- package/package.json +6 -2
- package/src/canvas-renderers/base-renderer.ts +11 -4
- package/src/canvas-renderers/container-renderer/index.ts +20 -23
- package/src/canvas-renderers/img-renderer/index.ts +2 -26
- package/src/canvas-renderers/shape-render/index.ts +279 -0
- package/src/canvas-renderers/text-renderer/types.ts +2 -44
- package/src/index.ts +11 -2
- package/src/types.ts +174 -12
- package/tsconfig.json +5 -3
- package/examples/sunscreen-guide.ts +0 -206
package/src/types.ts
CHANGED
|
@@ -1,25 +1,189 @@
|
|
|
1
|
-
import { ContainerRenderData } from "./canvas-renderers/container-renderer";
|
|
2
|
-
import { ImgRenderData } from "./canvas-renderers/img-renderer";
|
|
3
|
-
import { TextRenderData } from "./canvas-renderers/text-renderer/types";
|
|
4
1
|
import type { TextMetrics } from "canvas";
|
|
5
2
|
|
|
6
|
-
export
|
|
3
|
+
export enum RendererType {
|
|
4
|
+
CONTAINER = "container",
|
|
5
|
+
TEXT = "text",
|
|
6
|
+
IMG = "img",
|
|
7
|
+
SHAPE = "shape",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ----- TypeScript types (primitives only) -----
|
|
11
|
+
|
|
12
|
+
export interface TextRenderData {
|
|
13
|
+
id: string | number;
|
|
14
|
+
type: "text";
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
content: string;
|
|
20
|
+
rotate?: number;
|
|
21
|
+
style: {
|
|
22
|
+
align?: "center" | "right";
|
|
23
|
+
verticalAlign?: "center" | "top" | "bottom";
|
|
24
|
+
fontName: string;
|
|
25
|
+
fontSize: number | { max: number; min: number };
|
|
26
|
+
backgroundColor?: string;
|
|
27
|
+
padding?: number | { x: number; y: number };
|
|
28
|
+
border?: { color: string; width?: number };
|
|
29
|
+
color: string;
|
|
30
|
+
radius?: number;
|
|
31
|
+
verticalGap?: number;
|
|
32
|
+
horizonalGap?: number;
|
|
33
|
+
fontWeight?: string;
|
|
34
|
+
highlight?: {
|
|
35
|
+
logics?: string;
|
|
36
|
+
color?: string;
|
|
37
|
+
content?: string;
|
|
38
|
+
type?: string;
|
|
39
|
+
style?: { height?: number; offsetY?: number; coverText?: boolean; url: string };
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ImgRenderData {
|
|
45
|
+
id: string;
|
|
46
|
+
type: "img";
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
width?: number;
|
|
50
|
+
height?: number;
|
|
51
|
+
url?: string;
|
|
52
|
+
color?: string;
|
|
53
|
+
objectFit: "contain" | "cover";
|
|
54
|
+
radius?: number;
|
|
55
|
+
rotate?: number;
|
|
56
|
+
globalAlpha?: number;
|
|
57
|
+
shadow?: { color: string; blur: number; X: number; Y: number };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ContainerRenderData {
|
|
61
|
+
id: string | number;
|
|
62
|
+
type: "container";
|
|
63
|
+
x: number;
|
|
64
|
+
y: number;
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
direction?: "row" | "column";
|
|
68
|
+
itemAlign?: "center";
|
|
69
|
+
gap?: number | { x: number; y: number };
|
|
70
|
+
wrap?: boolean;
|
|
71
|
+
renderers: ChildRenderers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ShapeRenderData {
|
|
75
|
+
id: string | number;
|
|
76
|
+
type: "shape";
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
width?: number;
|
|
80
|
+
height?: number;
|
|
81
|
+
rotate?: number;
|
|
82
|
+
style?: {
|
|
83
|
+
fillStyle?: string;
|
|
84
|
+
strokeStyle?: string;
|
|
85
|
+
lineWidth?: number;
|
|
86
|
+
lineCap?: "butt" | "round" | "square";
|
|
87
|
+
lineJoin?: "bevel" | "round" | "miter";
|
|
88
|
+
miterLimit?: number;
|
|
89
|
+
lineDash?: number[];
|
|
90
|
+
lineDashOffset?: number;
|
|
91
|
+
globalAlpha?: number;
|
|
92
|
+
};
|
|
93
|
+
shadow?: {
|
|
94
|
+
color: string;
|
|
95
|
+
blur: number;
|
|
96
|
+
X: number;
|
|
97
|
+
Y: number;
|
|
98
|
+
};
|
|
99
|
+
shapes: ShapeCommand[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ShapeCommand =
|
|
103
|
+
| { type: "rect"; x: number; y: number; width: number; height: number }
|
|
104
|
+
| { type: "fillRect"; x: number; y: number; width: number; height: number }
|
|
105
|
+
| { type: "strokeRect"; x: number; y: number; width: number; height: number }
|
|
106
|
+
| { type: "clearRect"; x: number; y: number; width: number; height: number }
|
|
107
|
+
| { type: "beginPath" }
|
|
108
|
+
| { type: "closePath" }
|
|
109
|
+
| { type: "moveTo"; x: number; y: number }
|
|
110
|
+
| { type: "lineTo"; x: number; y: number }
|
|
111
|
+
| {
|
|
112
|
+
type: "arc";
|
|
113
|
+
x: number;
|
|
114
|
+
y: number;
|
|
115
|
+
radius: number;
|
|
116
|
+
startAngle: number;
|
|
117
|
+
endAngle: number;
|
|
118
|
+
counterclockwise?: boolean;
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: "arcTo";
|
|
122
|
+
x1: number;
|
|
123
|
+
y1: number;
|
|
124
|
+
x2: number;
|
|
125
|
+
y2: number;
|
|
126
|
+
radius: number;
|
|
127
|
+
}
|
|
128
|
+
| {
|
|
129
|
+
type: "quadraticCurveTo";
|
|
130
|
+
cp1x: number;
|
|
131
|
+
cp1y: number;
|
|
132
|
+
x: number;
|
|
133
|
+
y: number;
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
type: "bezierCurveTo";
|
|
137
|
+
cp1x: number;
|
|
138
|
+
cp1y: number;
|
|
139
|
+
cp2x: number;
|
|
140
|
+
cp2y: number;
|
|
141
|
+
x: number;
|
|
142
|
+
y: number;
|
|
143
|
+
}
|
|
144
|
+
| { type: "fill" }
|
|
145
|
+
| { type: "stroke" }
|
|
146
|
+
| { type: "fillAndStroke" };
|
|
147
|
+
|
|
148
|
+
export type ChildRenderers = (
|
|
149
|
+
| ImgRenderData
|
|
150
|
+
| TextRenderData
|
|
151
|
+
| ContainerRenderData
|
|
152
|
+
| ShapeRenderData
|
|
153
|
+
)[];
|
|
7
154
|
|
|
8
155
|
export interface RenderData {
|
|
9
156
|
id: string;
|
|
10
157
|
width: number;
|
|
11
158
|
height: number;
|
|
12
|
-
|
|
159
|
+
layers: (
|
|
160
|
+
| ImgRenderData
|
|
161
|
+
| TextRenderData
|
|
162
|
+
| ContainerRenderData
|
|
163
|
+
| ShapeRenderData
|
|
164
|
+
)[];
|
|
13
165
|
output?: {
|
|
14
166
|
type?: "png" | "jpg";
|
|
15
167
|
};
|
|
16
168
|
}
|
|
17
169
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
170
|
+
// ----- String schema for AI (readable as string) -----
|
|
171
|
+
|
|
172
|
+
export const RENDER_DATA_SCHEMA = `
|
|
173
|
+
RenderData: { "id": string, "width": number, "height": number, "layers": Array<TextRenderData | ImgRenderData | ContainerRenderData | ShapeRenderData>, "output"?: { "type"?: "png" | "jpg" } }
|
|
174
|
+
|
|
175
|
+
TextRenderData: { "id": string|number, "type": "text", "x": number, "y": number, "width": number, "height": number, "content": string, "style": { "fontName": string, "fontSize": number | { "max": number, "min": number }, "color": string, "align"?: "center"|"right", "verticalAlign"?: "center"|"top"|"bottom", "fontWeight"?: string, "verticalGap"?: number, "backgroundColor"?: string, "padding"?: number|{ "x": number, "y": number }, "border"?: { "color": string, "width"?: number }, "radius"?: number }, "rotate"?: number }
|
|
176
|
+
|
|
177
|
+
ImgRenderData: { "id": string, "type": "img", "x": number, "y": number, "width"?: number, "height"?: number, "url"?: string, "color"?: string, "objectFit": "contain"|"cover", "radius"?: number, "rotate"?: number, "globalAlpha"?: number, "shadow"?: { "color": string, "blur": number, "X": number, "Y": number } }
|
|
178
|
+
|
|
179
|
+
ContainerRenderData: { "id": string|number, "type": "container", "x": number, "y": number, "width": number, "height": number, "renderers": ChildRenderers[], "direction"?: "row"|"column", "gap"?: number|{ "x": number, "y": number }, "itemAlign"?: "center", "wrap"?: boolean }
|
|
180
|
+
|
|
181
|
+
ShapeRenderData: { "id": string|number, "type": "shape", "x": number, "y": number, "width"?: number, "height"?: number, "rotate"?: number, "style"?: { "fillStyle"?: string, "strokeStyle"?: string, "lineWidth"?: number, "lineCap"?: "butt"|"round"|"square", "lineJoin"?: "bevel"|"round"|"miter", "miterLimit"?: number, "lineDash"?: number[], "lineDashOffset"?: number, "globalAlpha"?: number }, "shadow"?: { "color": string, "blur": number, "X": number, "Y": number }, "shapes": Array<ShapeCommand> }
|
|
182
|
+
|
|
183
|
+
ShapeCommand: { "type": "rect"|"fillRect"|"strokeRect"|"clearRect"|"beginPath"|"closePath"|"moveTo"|"lineTo"|"arc"|"arcTo"|"quadraticCurveTo"|"bezierCurveTo"|"fill"|"stroke"|"fillAndStroke", ...additional properties based on type }
|
|
184
|
+
`.trim();
|
|
185
|
+
|
|
186
|
+
// ----- Metrics (canvas-dependent) -----
|
|
23
187
|
|
|
24
188
|
export interface MetricsChar {
|
|
25
189
|
char: string;
|
|
@@ -38,5 +202,3 @@ export interface MetricsCharWithCoordinates extends MetricsChar {
|
|
|
38
202
|
X: number;
|
|
39
203
|
Y: number;
|
|
40
204
|
}
|
|
41
|
-
|
|
42
|
-
export type ChildRenderers = (ImgRenderData | TextRenderData | ContainerRenderData)[];
|
package/tsconfig.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"lib": ["ES2020"],
|
|
6
|
-
"moduleResolution": "
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
7
|
"esModuleInterop": true,
|
|
8
8
|
"allowSyntheticDefaultImports": true,
|
|
9
9
|
"strict": true,
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
"forceConsistentCasingInFileNames": true,
|
|
12
12
|
"resolveJsonModule": true,
|
|
13
13
|
"isolatedModules": true,
|
|
14
|
-
"noEmit": true
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"types": ["node"],
|
|
16
|
+
"allowImportingTsExtensions": true
|
|
15
17
|
},
|
|
16
18
|
"ts-node": {
|
|
17
19
|
"esm": true,
|
|
18
20
|
"experimentalSpecifierResolution": "node"
|
|
19
21
|
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
22
|
+
"include": ["src/**/*", "examples/**/*"],
|
|
21
23
|
"exclude": ["node_modules"]
|
|
22
24
|
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { Renderer } from "../src/index";
|
|
2
|
-
import { RenderData, RendererType } from "../src/types";
|
|
3
|
-
import * as fs from "fs";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
|
|
10
|
-
// Content data in JSON format for easy reading and maintenance
|
|
11
|
-
const contentData = {
|
|
12
|
-
title: "防晒指南",
|
|
13
|
-
sections: [
|
|
14
|
-
{
|
|
15
|
-
question: "该不该防晒:",
|
|
16
|
-
answer: "适度防晒"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
question: "怎么防晒:",
|
|
20
|
-
answer: "做好物理防晒,选适配防晒霜(矿物款更安全,适合敏感肌/儿童,化学款慎选成分),避开正午强紫外线,早晚适度日晒。"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
question: "为什么适度:",
|
|
24
|
-
answer: "紫外线会致皮肤DNA突变、加速老化,防晒对护肤和防病至关重要;但完全避光不利健康,且长波长光能穿透防晒和衣物,足够满足合成需求,不用刻意不防晒。维生素D能够调节激素,还与更好的生活质量和寿命相关。"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Dark & Elegant theme canvas configuration (4:3 aspect ratio)
|
|
30
|
-
const CANVAS_CONFIG = {
|
|
31
|
-
width: 1200,
|
|
32
|
-
height: 900, // 4:3 ratio
|
|
33
|
-
padding: 50,
|
|
34
|
-
colors: {
|
|
35
|
-
background: "#121212", // Deep black
|
|
36
|
-
title: "#FFD700", // Gold
|
|
37
|
-
question: "#FFFFFF", // White
|
|
38
|
-
answer: "#B0B0B0", // Light Gray
|
|
39
|
-
cardBg: "#1E1E1E", // Dark Gray Card
|
|
40
|
-
accent: "#FFD700", // Gold
|
|
41
|
-
highlight: "#333333" // Subtle Dark Circle
|
|
42
|
-
},
|
|
43
|
-
fonts: {
|
|
44
|
-
title: "PingFang SC",
|
|
45
|
-
body: "PingFang SC"
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// Calculate text height based on content and width
|
|
50
|
-
function calcTextHeight(text: string, fontSize: number, containerWidth: number, lineGap = 8): number {
|
|
51
|
-
// CJK characters are roughly square (width ≈ fontSize)
|
|
52
|
-
const charsPerLine = Math.floor(containerWidth / fontSize);
|
|
53
|
-
const lines = Math.ceil(text.length / charsPerLine);
|
|
54
|
-
return lines * (fontSize + lineGap);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Build renderers from content data
|
|
58
|
-
function buildRenderers() {
|
|
59
|
-
const renderers: RenderData["renderers"] = [];
|
|
60
|
-
const contentWidth = CANVAS_CONFIG.width - CANVAS_CONFIG.padding * 2;
|
|
61
|
-
let currentY = CANVAS_CONFIG.padding;
|
|
62
|
-
|
|
63
|
-
// 1. Background
|
|
64
|
-
renderers.push({
|
|
65
|
-
id: "background",
|
|
66
|
-
type: RendererType.IMG,
|
|
67
|
-
x: 0,
|
|
68
|
-
y: 0,
|
|
69
|
-
width: CANVAS_CONFIG.width,
|
|
70
|
-
height: CANVAS_CONFIG.height,
|
|
71
|
-
color: CANVAS_CONFIG.colors.background,
|
|
72
|
-
objectFit: "cover"
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// 2. Decorative Dark Sun Circle (top right)
|
|
76
|
-
renderers.push({
|
|
77
|
-
id: "sun-circle",
|
|
78
|
-
type: RendererType.IMG,
|
|
79
|
-
x: CANVAS_CONFIG.width - 180,
|
|
80
|
-
y: -60,
|
|
81
|
-
width: 240,
|
|
82
|
-
height: 240,
|
|
83
|
-
color: CANVAS_CONFIG.colors.highlight,
|
|
84
|
-
radius: 120,
|
|
85
|
-
objectFit: "cover"
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// 3. Title
|
|
89
|
-
const titleHeight = 80;
|
|
90
|
-
renderers.push({
|
|
91
|
-
id: "title",
|
|
92
|
-
type: RendererType.TEXT,
|
|
93
|
-
x: CANVAS_CONFIG.padding,
|
|
94
|
-
y: currentY,
|
|
95
|
-
width: contentWidth,
|
|
96
|
-
height: titleHeight,
|
|
97
|
-
content: "☀️ " + contentData.title,
|
|
98
|
-
style: {
|
|
99
|
-
fontName: CANVAS_CONFIG.fonts.title,
|
|
100
|
-
fontSize: 48,
|
|
101
|
-
color: CANVAS_CONFIG.colors.title,
|
|
102
|
-
fontWeight: "bold",
|
|
103
|
-
verticalAlign: "center"
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
currentY += titleHeight + 20;
|
|
107
|
-
|
|
108
|
-
// 4. Content Sections
|
|
109
|
-
const questionFontSize = 24;
|
|
110
|
-
const answerFontSize = 20;
|
|
111
|
-
const cardPadding = 24;
|
|
112
|
-
|
|
113
|
-
contentData.sections.forEach((section, index) => {
|
|
114
|
-
// Card background first (dark rounded rectangle)
|
|
115
|
-
const answerTextWidth = contentWidth - cardPadding * 2;
|
|
116
|
-
const textHeight = calcTextHeight(section.answer, answerFontSize, answerTextWidth, 12);
|
|
117
|
-
const cardHeight = textHeight + cardPadding * 2 + 50; // Extra space for question
|
|
118
|
-
|
|
119
|
-
renderers.push({
|
|
120
|
-
id: `card-bg-${index}`,
|
|
121
|
-
type: RendererType.IMG,
|
|
122
|
-
x: CANVAS_CONFIG.padding,
|
|
123
|
-
y: currentY,
|
|
124
|
-
width: contentWidth,
|
|
125
|
-
height: cardHeight,
|
|
126
|
-
color: CANVAS_CONFIG.colors.cardBg,
|
|
127
|
-
radius: 16,
|
|
128
|
-
objectFit: "cover"
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Question label inside card
|
|
132
|
-
const questionY = currentY + cardPadding;
|
|
133
|
-
renderers.push({
|
|
134
|
-
id: `q-${index}`,
|
|
135
|
-
type: RendererType.TEXT,
|
|
136
|
-
x: CANVAS_CONFIG.padding + cardPadding,
|
|
137
|
-
y: questionY,
|
|
138
|
-
width: contentWidth - cardPadding * 2,
|
|
139
|
-
height: 32,
|
|
140
|
-
content: "▸ " + section.question,
|
|
141
|
-
style: {
|
|
142
|
-
fontName: CANVAS_CONFIG.fonts.body,
|
|
143
|
-
fontSize: questionFontSize,
|
|
144
|
-
color: CANVAS_CONFIG.colors.question,
|
|
145
|
-
fontWeight: "bold",
|
|
146
|
-
verticalAlign: "center"
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Answer text inside card
|
|
151
|
-
const answerY = questionY + 40;
|
|
152
|
-
renderers.push({
|
|
153
|
-
id: `a-${index}`,
|
|
154
|
-
type: RendererType.TEXT,
|
|
155
|
-
x: CANVAS_CONFIG.padding + cardPadding,
|
|
156
|
-
y: answerY,
|
|
157
|
-
width: answerTextWidth,
|
|
158
|
-
height: textHeight + 20,
|
|
159
|
-
content: section.answer,
|
|
160
|
-
style: {
|
|
161
|
-
fontName: CANVAS_CONFIG.fonts.body,
|
|
162
|
-
fontSize: answerFontSize,
|
|
163
|
-
color: CANVAS_CONFIG.colors.answer,
|
|
164
|
-
verticalGap: 12,
|
|
165
|
-
verticalAlign: "top"
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
currentY += cardHeight + 24;
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return renderers;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Create canvas schema
|
|
176
|
-
const schema: RenderData = {
|
|
177
|
-
id: "sunscreen-guide",
|
|
178
|
-
width: CANVAS_CONFIG.width,
|
|
179
|
-
height: CANVAS_CONFIG.height,
|
|
180
|
-
renderers: buildRenderers(),
|
|
181
|
-
output: {
|
|
182
|
-
type: "png"
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Main execution
|
|
187
|
-
async function main() {
|
|
188
|
-
console.log("Generating fresh sunscreen guide image...");
|
|
189
|
-
console.log("Content:", JSON.stringify(contentData, null, 2));
|
|
190
|
-
|
|
191
|
-
const renderer = new Renderer(schema);
|
|
192
|
-
const buffer = await renderer.draw();
|
|
193
|
-
|
|
194
|
-
// Ensure output directory exists
|
|
195
|
-
const outputDir = path.join(__dirname, "output");
|
|
196
|
-
if (!fs.existsSync(outputDir)) {
|
|
197
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const outputPath = path.join(outputDir, "sunscreen-guide.png");
|
|
201
|
-
fs.writeFileSync(outputPath, buffer);
|
|
202
|
-
|
|
203
|
-
console.log(`Image saved to: ${outputPath}`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
main().catch(console.error);
|