@lemonppt/renderer 1.0.7 → 1.0.8

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.
Files changed (84) hide show
  1. package/dist/browser-export-driver.d.ts +28 -0
  2. package/dist/browser-export-driver.d.ts.map +1 -0
  3. package/dist/browser-export-driver.js +115 -0
  4. package/dist/browser-export-driver.js.map +1 -0
  5. package/dist/browser-export.d.ts +66 -0
  6. package/dist/browser-export.d.ts.map +1 -0
  7. package/dist/browser-export.js +176 -0
  8. package/dist/browser-export.js.map +1 -0
  9. package/dist/client/browser-export.js +1 -0
  10. package/dist/client/client-render.js +21 -21
  11. package/dist/client/theme-echarts.js +27 -27
  12. package/dist/export-pdf.d.ts.map +1 -1
  13. package/dist/export-pdf.js +6 -3
  14. package/dist/export-pdf.js.map +1 -1
  15. package/dist/export-pptx-screenshot.d.ts +36 -0
  16. package/dist/export-pptx-screenshot.d.ts.map +1 -0
  17. package/dist/export-pptx-screenshot.js +74 -0
  18. package/dist/export-pptx-screenshot.js.map +1 -0
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/normalize-goal.d.ts +6 -0
  24. package/dist/normalize-goal.d.ts.map +1 -0
  25. package/dist/normalize-goal.js +12 -0
  26. package/dist/normalize-goal.js.map +1 -0
  27. package/dist/preview-server.d.ts +9 -0
  28. package/dist/preview-server.d.ts.map +1 -0
  29. package/dist/preview-server.js +81 -0
  30. package/dist/preview-server.js.map +1 -0
  31. package/dist/render-editor-data.js +2 -2
  32. package/dist/render-editor-data.js.map +1 -1
  33. package/dist/render.d.ts.map +1 -1
  34. package/dist/render.js +14 -4
  35. package/dist/render.js.map +1 -1
  36. package/package.json +8 -5
  37. package/dist/export-pptx.d.ts +0 -1167
  38. package/dist/export-pptx.d.ts.map +0 -1
  39. package/dist/export-pptx.js +0 -3007
  40. package/dist/export-pptx.js.map +0 -1
  41. package/dist/theme01-pptx.d.ts +0 -12
  42. package/dist/theme01-pptx.d.ts.map +0 -1
  43. package/dist/theme01-pptx.js +0 -2957
  44. package/dist/theme01-pptx.js.map +0 -1
  45. package/dist/theme02-pptx.d.ts +0 -5
  46. package/dist/theme02-pptx.d.ts.map +0 -1
  47. package/dist/theme02-pptx.js +0 -1063
  48. package/dist/theme02-pptx.js.map +0 -1
  49. package/dist/theme03-pptx.d.ts +0 -5
  50. package/dist/theme03-pptx.d.ts.map +0 -1
  51. package/dist/theme03-pptx.js +0 -3279
  52. package/dist/theme03-pptx.js.map +0 -1
  53. package/dist/theme04-pptx.d.ts +0 -6
  54. package/dist/theme04-pptx.d.ts.map +0 -1
  55. package/dist/theme04-pptx.js +0 -5496
  56. package/dist/theme04-pptx.js.map +0 -1
  57. package/dist/theme05-pptx.d.ts +0 -5
  58. package/dist/theme05-pptx.d.ts.map +0 -1
  59. package/dist/theme05-pptx.js +0 -3152
  60. package/dist/theme05-pptx.js.map +0 -1
  61. package/dist/theme06-pptx.d.ts +0 -124
  62. package/dist/theme06-pptx.d.ts.map +0 -1
  63. package/dist/theme06-pptx.js +0 -8154
  64. package/dist/theme06-pptx.js.map +0 -1
  65. package/dist/theme07-pptx.d.ts +0 -49
  66. package/dist/theme07-pptx.d.ts.map +0 -1
  67. package/dist/theme07-pptx.js +0 -2579
  68. package/dist/theme07-pptx.js.map +0 -1
  69. package/dist/theme08-pptx.d.ts +0 -19
  70. package/dist/theme08-pptx.d.ts.map +0 -1
  71. package/dist/theme08-pptx.js +0 -1411
  72. package/dist/theme08-pptx.js.map +0 -1
  73. package/dist/theme09-pptx.d.ts +0 -18
  74. package/dist/theme09-pptx.d.ts.map +0 -1
  75. package/dist/theme09-pptx.js +0 -1513
  76. package/dist/theme09-pptx.js.map +0 -1
  77. package/dist/theme10-pptx.d.ts +0 -18
  78. package/dist/theme10-pptx.d.ts.map +0 -1
  79. package/dist/theme10-pptx.js +0 -2889
  80. package/dist/theme10-pptx.js.map +0 -1
  81. package/dist/theme11-pptx.d.ts +0 -17
  82. package/dist/theme11-pptx.d.ts.map +0 -1
  83. package/dist/theme11-pptx.js +0 -908
  84. package/dist/theme11-pptx.js.map +0 -1
@@ -1,1411 +0,0 @@
1
- // lemonPPT - AI-powered presentation generation
2
- // Copyright (c) 2026 lemonforme
3
- // SPDX-License-Identifier: AGPL-3.0-or-later
4
- //
5
- // theme08 黑金实验风 — PPTX 版式专属渲染器。
6
- // 解决此前 theme08 在 PPTX 导出中 37/39 版式落到 "Unknown layout" 占位的问题。
7
- // 所有渲染器基于 theme08 设计语言:深黑底 + 荧光金主强调(primary)/ 象牙暖白 + 古铜金(muted)。
8
- let S;
9
- /** 每次导出开始时由 export-pptx.ts 注入当前主题的配色/字体/深浅模式。 */
10
- export function configureTheme08(state) {
11
- S = state;
12
- }
13
- // ---- 画布常量(16:9,10 x 5.625 英寸) ----
14
- const MX = 0.7; // 左右页边距
15
- const CW = 10 - MX * 2; // 内容宽度 8.6
16
- const BODY_TOP = 2.05; // 正文区起始 y
17
- // ---- 颜色速取 ----
18
- const ink = () => S.colors.primary;
19
- const ink2 = () => S.colors.secondary;
20
- const gold = () => S.colors.accent;
21
- const surface = () => S.colors.surface;
22
- const surfaceElev = () => S.colors.surfaceElevated;
23
- const border = () => S.colors.border;
24
- const isDark = () => !S.muted;
25
- const mono = () => S.fonts.mono;
26
- const heading = () => S.fonts.heading;
27
- const body = () => S.fonts.body;
28
- // ---- 基础绘制 ----
29
- function txt(slide, text, o) {
30
- if (text == null || text === '')
31
- return;
32
- slide.addText(text, o);
33
- }
34
- /** 设置主题背景 + 顶部金色装饰条。 */
35
- function bg(slide) {
36
- slide.background = { color: surface() };
37
- slide.addShape('rect', { x: 0, y: 0, w: 10, h: 0.1, fill: { color: gold() } });
38
- // 底部细线
39
- slide.addShape('line', {
40
- x: MX, y: 5.32, w: CW, h: 0,
41
- line: { color: border(), width: 0.75 },
42
- });
43
- }
44
- function kicker(slide, text, x = MX, y = 0.45) {
45
- txt(slide, text, {
46
- x, y, w: CW, h: 0.35, fontSize: 13, color: gold(), align: 'left',
47
- fontFace: mono(), charSpacing: 2, bold: true,
48
- });
49
- }
50
- function title(slide, text, x = MX, y = 0.82, w = CW, size = 30) {
51
- txt(slide, text, {
52
- x, y, w, h: 0.9, fontSize: size, color: ink(), align: 'left',
53
- fontFace: heading(), bold: true, valign: 'top',
54
- });
55
- }
56
- function subtitle(slide, text, x = MX, y = 1.62, w = CW) {
57
- txt(slide, text, {
58
- x, y, w, h: 0.4, fontSize: 15, color: ink2(), align: 'left', fontFace: body(),
59
- });
60
- }
61
- function footer(slide, left, right) {
62
- txt(slide, left, { x: MX, y: 5.34, w: CW / 2, h: 0.25, fontSize: 9, color: ink2(), align: 'left', fontFace: mono() });
63
- txt(slide, right, { x: MX + CW / 2, y: 5.34, w: CW / 2, h: 0.25, fontSize: 9, color: ink2(), align: 'right', fontFace: mono() });
64
- }
65
- /** 圆角卡片背景。 */
66
- function card(slide, x, y, w, h, fill, stroke) {
67
- slide.addShape('roundRect', {
68
- x, y, w, h, rectRadius: 0.08,
69
- fill: { color: fill || surfaceElev() },
70
- line: { color: stroke || border(), width: 1 },
71
- });
72
- }
73
- /** 网格列宽计算。 */
74
- function cols(n, max = 4) {
75
- return Math.min(n, max);
76
- }
77
- // =====================================================================
78
- // 1. cover
79
- // =====================================================================
80
- function renderTheme08Cover(slide, p) {
81
- bg(slide);
82
- if (p.imageUrl) {
83
- slide.addImage({ path: p.imageUrl, x: 0, y: 0, w: 10, h: 5.625 });
84
- slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: isDark() ? '14130D' : 'FDFBF4', transparency: 35 } });
85
- }
86
- if (p.tag) {
87
- slide.addShape('roundRect', { x: MX, y: 1.0, w: 0.4 + p.tag.length * 0.16, h: 0.42, rectRadius: 0.21, fill: { color: gold() } });
88
- txt(slide, p.tag, { x: MX, y: 1.0, w: 0.4 + p.tag.length * 0.16, h: 0.42, fontSize: 13, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', valign: 'middle', bold: true, fontFace: mono() });
89
- }
90
- txt(slide, p.title, { x: MX, y: 1.65, w: CW, h: 1.5, fontSize: 46, color: ink(), align: 'left', fontFace: heading(), bold: true, valign: 'top' });
91
- txt(slide, p.subtitle, { x: MX, y: 3.2, w: CW, h: 0.8, fontSize: 18, color: ink2(), align: 'left', fontFace: body() });
92
- const metrics = (p.metrics || []).slice(0, 4);
93
- if (metrics.length) {
94
- const mw = CW / metrics.length;
95
- metrics.forEach((m, i) => {
96
- const x = MX + i * mw;
97
- const val = [m.value, m.unit].filter(Boolean).join(' ');
98
- txt(slide, val, { x, y: 4.25, w: mw, h: 0.5, fontSize: 26, color: m.accent ? gold() : ink(), align: 'left', bold: true, fontFace: heading() });
99
- txt(slide, m.label, { x, y: 4.78, w: mw, h: 0.35, fontSize: 12, color: ink2(), align: 'left', fontFace: body() });
100
- });
101
- }
102
- if (p.tags && p.tags.length) {
103
- txt(slide, p.tags.join(' · '), { x: MX, y: 5.1, w: CW, h: 0.3, fontSize: 11, color: ink2(), align: 'left', fontFace: mono() });
104
- }
105
- footer(slide, p.footnoteLeft, p.footnoteRight);
106
- }
107
- // =====================================================================
108
- // 2. chapter
109
- // =====================================================================
110
- function renderTheme08Chapter(slide, p) {
111
- bg(slide);
112
- if (p.index) {
113
- txt(slide, p.index, { x: MX, y: 1.4, w: 3, h: 2.4, fontSize: 150, color: gold(), align: 'left', valign: 'middle', bold: true, fontFace: heading() });
114
- }
115
- if (p.kicker) {
116
- txt(slide, p.kicker, { x: 4.2, y: 1.9, w: 5, h: 0.4, fontSize: 14, color: gold(), align: 'left', fontFace: mono(), bold: true, charSpacing: 2 });
117
- }
118
- txt(slide, p.title, { x: 4.2, y: 2.35, w: 5.1, h: 1.0, fontSize: 38, color: ink(), align: 'left', valign: 'top', bold: true, fontFace: heading() });
119
- txt(slide, p.subtitle, { x: 4.2, y: 3.4, w: 5.1, h: 0.8, fontSize: 16, color: ink2(), align: 'left', fontFace: body() });
120
- footer(slide, p.footnoteLeft, p.footnoteRight);
121
- }
122
- // =====================================================================
123
- // 3. content
124
- // =====================================================================
125
- function renderTheme08Content(slide, p) {
126
- bg(slide);
127
- kicker(slide, p.kicker);
128
- title(slide, p.title);
129
- subtitle(slide, p.subtitle);
130
- const items = (p.items || []).slice(0, 4);
131
- const n = cols(items.length, 2);
132
- const gap = 0.3;
133
- const cw = (CW - gap * (n - 1)) / n;
134
- const ch = 1.35;
135
- items.forEach((it, i) => {
136
- const r = Math.floor(i / n);
137
- const c = i % n;
138
- const x = MX + c * (cw + gap);
139
- const y = BODY_TOP + r * (ch + 0.25);
140
- card(slide, x, y, cw, ch);
141
- txt(slide, it.title, { x: x + 0.25, y: y + 0.18, w: cw - 0.5, h: 0.45, fontSize: 17, color: ink(), bold: true, align: 'left', fontFace: heading() });
142
- txt(slide, it.desc, { x: x + 0.25, y: y + 0.65, w: cw - 0.5, h: ch - 0.8, fontSize: 12.5, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
143
- });
144
- const stats = (p.stats || []).slice(0, 4);
145
- if (stats.length) {
146
- const sy = BODY_TOP + 2 * (ch + 0.25) + 0.05;
147
- const sw = CW / stats.length;
148
- stats.forEach((st, i) => {
149
- const x = MX + i * sw;
150
- txt(slide, [st.num, st.label].filter(Boolean).join(' '), { x, y: sy, w: sw, h: 0.5, fontSize: 16, color: gold(), bold: true, align: 'left', fontFace: heading() });
151
- });
152
- }
153
- footer(slide, p.footnoteLeft, p.footnoteRight);
154
- }
155
- // =====================================================================
156
- // 4. contents(目录)
157
- // =====================================================================
158
- function renderTheme08Contents(slide, p) {
159
- bg(slide);
160
- kicker(slide, p.kicker);
161
- title(slide, p.title);
162
- subtitle(slide, p.subtitle);
163
- const items = (p.items || []).slice(0, 6);
164
- let y = BODY_TOP;
165
- const rowH = (5.1 - BODY_TOP) / Math.max(items.length, 1);
166
- items.forEach((it, i) => {
167
- const num = String(i + 1).padStart(2, '0');
168
- txt(slide, num, { x: MX, y, w: 1.0, h: rowH, fontSize: 26, color: gold(), align: 'left', valign: 'middle', bold: true, fontFace: mono() });
169
- txt(slide, it.title, { x: MX + 1.1, y, w: CW - 1.1, h: rowH, fontSize: 22, color: ink(), align: 'left', valign: 'middle', fontFace: body() });
170
- slide.addShape('line', { x: MX + 1.1, y: y + rowH, w: CW - 1.1, h: 0, line: { color: border(), width: 0.5 } });
171
- y += rowH;
172
- });
173
- footer(slide, p.footnoteLeft, p.footnoteRight);
174
- }
175
- // =====================================================================
176
- // 5. metrics(指标卡)
177
- // =====================================================================
178
- function renderTheme08Metrics(slide, p) {
179
- bg(slide);
180
- kicker(slide, p.kicker);
181
- title(slide, p.title);
182
- subtitle(slide, p.subtitle);
183
- const items = (p.items || []).slice(0, 4);
184
- const n = cols(items.length, 4);
185
- const gap = 0.3;
186
- const cw = (CW - gap * (n - 1)) / n;
187
- const ch = 2.4;
188
- items.forEach((it, i) => {
189
- const x = MX + i * (cw + gap);
190
- card(slide, x, BODY_TOP, cw, ch, it.accent ? gold() : undefined);
191
- const tcol = it.accent ? (isDark() ? '14130D' : 'FFFFFF') : ink();
192
- txt(slide, [it.value, it.unit].filter(Boolean).join(' '), { x: x + 0.2, y: BODY_TOP + 0.4, w: cw - 0.4, h: 0.9, fontSize: 34, color: tcol, align: 'center', bold: true, fontFace: heading() });
193
- txt(slide, it.label, { x: x + 0.2, y: BODY_TOP + 1.4, w: cw - 0.4, h: 0.8, fontSize: 13, color: tcol, align: 'center', valign: 'top', fontFace: body() });
194
- });
195
- footer(slide, p.footnoteLeft, p.footnoteRight);
196
- }
197
- // =====================================================================
198
- // 6. metric-big
199
- // =====================================================================
200
- function renderTheme08MetricBig(slide, p) {
201
- bg(slide);
202
- kicker(slide, p.kicker, MX, 1.4);
203
- const val = [p.value, p.unit].filter(Boolean).join(' ');
204
- txt(slide, val, { x: MX, y: 1.9, w: CW, h: 1.8, fontSize: 100, color: gold(), align: 'left', bold: true, fontFace: heading(), valign: 'middle' });
205
- txt(slide, p.title, { x: MX, y: 3.8, w: CW, h: 0.6, fontSize: 24, color: ink(), align: 'left', fontFace: heading(), bold: true });
206
- txt(slide, p.desc, { x: MX, y: 4.45, w: CW, h: 0.7, fontSize: 15, color: ink2(), align: 'left', fontFace: body() });
207
- footer(slide, p.footnoteLeft, p.footnoteRight);
208
- }
209
- // =====================================================================
210
- // 7. feature
211
- // =====================================================================
212
- function renderTheme08Feature(slide, p) {
213
- bg(slide);
214
- kicker(slide, p.kicker);
215
- title(slide, p.title);
216
- subtitle(slide, p.subtitle);
217
- const items = (p.items || []).slice(0, 6);
218
- const n = cols(items.length, 3);
219
- const gap = 0.3;
220
- const cw = (CW - gap * (n - 1)) / n;
221
- const ch = 2.55;
222
- items.forEach((it, i) => {
223
- const r = Math.floor(i / n);
224
- const c = i % n;
225
- const x = MX + c * (cw + gap);
226
- const y = BODY_TOP + r * (ch + 0.25);
227
- card(slide, x, y, cw, ch);
228
- if (it.icon)
229
- txt(slide, it.icon, { x: x + 0.25, y: y + 0.22, w: 0.8, h: 0.6, fontSize: 28, align: 'left', fontFace: body() });
230
- txt(slide, it.title, { x: x + 0.25, y: y + 0.95, w: cw - 0.5, h: 0.45, fontSize: 16, color: ink(), bold: true, align: 'left', fontFace: heading() });
231
- txt(slide, it.desc, { x: x + 0.25, y: y + 1.4, w: cw - 0.5, h: ch - 1.5, fontSize: 12, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
232
- });
233
- footer(slide, p.footnoteLeft, p.footnoteRight);
234
- }
235
- // =====================================================================
236
- // 8. overview
237
- // =====================================================================
238
- function renderTheme08Overview(slide, p) {
239
- bg(slide);
240
- kicker(slide, p.kicker);
241
- title(slide, p.title);
242
- subtitle(slide, p.subtitle);
243
- const items = (p.items || []).slice(0, 4);
244
- const n = cols(items.length, 2);
245
- const gap = 0.3;
246
- const cw = (CW - gap * (n - 1)) / n;
247
- const ch = 1.45;
248
- items.forEach((it, i) => {
249
- const r = Math.floor(i / n);
250
- const c = i % n;
251
- const x = MX + c * (cw + gap);
252
- const y = BODY_TOP + r * (ch + 0.25);
253
- card(slide, x, y, cw, ch);
254
- txt(slide, it.num, { x: x + 0.25, y: y + 0.2, w: 1.2, h: 1.0, fontSize: 40, color: gold(), align: 'left', valign: 'middle', bold: true, fontFace: heading() });
255
- txt(slide, it.label, { x: x + 1.55, y: y + 0.2, w: cw - 1.8, h: 0.5, fontSize: 16, color: ink(), bold: true, align: 'left', fontFace: heading() });
256
- txt(slide, it.desc, { x: x + 1.55, y: y + 0.7, w: cw - 1.8, h: ch - 0.85, fontSize: 12, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
257
- });
258
- footer(slide, p.footnoteLeft, p.footnoteRight);
259
- }
260
- // =====================================================================
261
- // 9. strategy
262
- // =====================================================================
263
- function renderTheme08Strategy(slide, p) {
264
- bg(slide);
265
- kicker(slide, p.kicker);
266
- title(slide, p.title);
267
- subtitle(slide, p.subtitle);
268
- const items = (p.items || []).slice(0, 4);
269
- const n = cols(items.length, 2);
270
- const gap = 0.3;
271
- const cw = (CW - gap * (n - 1)) / n;
272
- const ch = 1.4;
273
- items.forEach((it, i) => {
274
- const r = Math.floor(i / n);
275
- const c = i % n;
276
- const x = MX + c * (cw + gap);
277
- const y = BODY_TOP + r * (ch + 0.25);
278
- card(slide, x, y, cw, ch);
279
- slide.addShape('rect', { x, y, w: 0.08, h: ch, fill: { color: gold() } });
280
- txt(slide, it.title, { x: x + 0.3, y: y + 0.2, w: cw - 0.5, h: 0.45, fontSize: 17, color: ink(), bold: true, align: 'left', fontFace: heading() });
281
- txt(slide, it.desc, { x: x + 0.3, y: y + 0.68, w: cw - 0.5, h: ch - 0.8, fontSize: 12.5, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
282
- });
283
- footer(slide, p.footnoteLeft, p.footnoteRight);
284
- }
285
- // =====================================================================
286
- // 10. process
287
- // =====================================================================
288
- function renderTheme08Process(slide, p) {
289
- bg(slide);
290
- kicker(slide, p.kicker);
291
- title(slide, p.title);
292
- subtitle(slide, p.subtitle);
293
- const items = (p.items || []).slice(0, 5);
294
- const n = items.length;
295
- const gap = 0.4;
296
- const cw = (CW - gap * (n - 1)) / n;
297
- items.forEach((it, i) => {
298
- const x = MX + i * (cw + gap);
299
- card(slide, x, BODY_TOP, cw, 2.6);
300
- txt(slide, String(i + 1).padStart(2, '0'), { x: x + 0.2, y: BODY_TOP + 0.2, w: cw - 0.4, h: 0.6, fontSize: 28, color: gold(), align: 'left', bold: true, fontFace: mono() });
301
- txt(slide, it.title, { x: x + 0.2, y: BODY_TOP + 0.85, w: cw - 0.4, h: 0.6, fontSize: 15, color: ink(), bold: true, align: 'left', valign: 'top', fontFace: heading() });
302
- txt(slide, it.desc, { x: x + 0.2, y: BODY_TOP + 1.5, w: cw - 0.4, h: 1.0, fontSize: 11.5, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
303
- if (i < n - 1) {
304
- txt(slide, '→', { x: x + cw + 0.02, y: BODY_TOP + 1.0, w: gap, h: 0.5, fontSize: 20, color: gold(), align: 'center', valign: 'middle', bold: true });
305
- }
306
- });
307
- footer(slide, p.footnoteLeft, p.footnoteRight);
308
- }
309
- // =====================================================================
310
- // 11. timeline
311
- // =====================================================================
312
- function renderTheme08Timeline(slide, p) {
313
- bg(slide);
314
- kicker(slide, p.kicker);
315
- title(slide, p.title);
316
- subtitle(slide, p.subtitle);
317
- const items = (p.items || []).slice(0, 5);
318
- const n = items.length;
319
- const lineY = BODY_TOP + 0.35;
320
- slide.addShape('line', { x: MX, y: lineY, w: CW, h: 0, line: { color: gold(), width: 1.5 } });
321
- const step = CW / n;
322
- items.forEach((it, i) => {
323
- const cx = MX + step * i + step / 2;
324
- slide.addShape('ellipse', { x: cx - 0.09, y: lineY - 0.09, w: 0.18, h: 0.18, fill: { color: gold() } });
325
- txt(slide, it.year, { x: cx - step / 2, y: lineY + 0.15, w: step, h: 0.35, fontSize: 15, color: gold(), align: 'center', bold: true, fontFace: mono() });
326
- txt(slide, it.title, { x: cx - step / 2 + 0.1, y: lineY + 0.55, w: step - 0.2, h: 0.45, fontSize: 14, color: ink(), bold: true, align: 'center', fontFace: heading() });
327
- txt(slide, it.desc, { x: cx - step / 2 + 0.1, y: lineY + 1.0, w: step - 0.2, h: 1.4, fontSize: 11, color: ink2(), align: 'center', valign: 'top', fontFace: body() });
328
- });
329
- footer(slide, p.footnoteLeft, p.footnoteRight);
330
- }
331
- // =====================================================================
332
- // 12. roadmap
333
- // =====================================================================
334
- function renderTheme08Roadmap(slide, p) {
335
- bg(slide);
336
- kicker(slide, p.kicker);
337
- title(slide, p.title);
338
- subtitle(slide, p.subtitle);
339
- const items = (p.items || []).slice(0, 4);
340
- const n = items.length;
341
- const gap = 0.3;
342
- const cw = (CW - gap * (n - 1)) / n;
343
- items.forEach((it, i) => {
344
- const x = MX + i * (cw + gap);
345
- card(slide, x, BODY_TOP, cw, 2.8);
346
- txt(slide, it.period, { x: x + 0.2, y: BODY_TOP + 0.2, w: cw - 0.4, h: 0.4, fontSize: 14, color: gold(), align: 'left', bold: true, fontFace: mono() });
347
- txt(slide, it.goal, { x: x + 0.2, y: BODY_TOP + 0.65, w: cw - 0.4, h: 0.6, fontSize: 15, color: ink(), bold: true, align: 'left', valign: 'top', fontFace: heading() });
348
- (it.points || []).slice(0, 4).forEach((pt, j) => {
349
- txt(slide, '· ' + pt, { x: x + 0.2, y: BODY_TOP + 1.3 + j * 0.36, w: cw - 0.4, h: 0.34, fontSize: 11, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
350
- });
351
- });
352
- footer(slide, p.footnoteLeft, p.footnoteRight);
353
- }
354
- // =====================================================================
355
- // 13. workflow
356
- // =====================================================================
357
- function renderTheme08Workflow(slide, p) {
358
- bg(slide);
359
- kicker(slide, p.kicker);
360
- title(slide, p.title);
361
- subtitle(slide, p.subtitle);
362
- const steps = (p.steps || []).slice(0, 5);
363
- const n = steps.length;
364
- const gap = 0.25;
365
- const cw = (CW - gap * (n - 1)) / n;
366
- steps.forEach((st, i) => {
367
- const x = MX + i * (cw + gap);
368
- card(slide, x, BODY_TOP, cw, 2.3);
369
- slide.addShape('ellipse', { x: x + cw / 2 - 0.3, y: BODY_TOP + 0.2, w: 0.6, h: 0.6, fill: { color: gold() } });
370
- txt(slide, st.num, { x: x + cw / 2 - 0.3, y: BODY_TOP + 0.2, w: 0.6, h: 0.6, fontSize: 20, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', valign: 'middle', bold: true, fontFace: heading() });
371
- txt(slide, st.title, { x: x + 0.15, y: BODY_TOP + 0.95, w: cw - 0.3, h: 0.5, fontSize: 13.5, color: ink(), bold: true, align: 'center', valign: 'top', fontFace: heading() });
372
- txt(slide, st.desc, { x: x + 0.15, y: BODY_TOP + 1.45, w: cw - 0.3, h: 0.8, fontSize: 10.5, color: ink2(), align: 'center', valign: 'top', fontFace: body() });
373
- });
374
- const labels = p.timelineLabels || [];
375
- if (labels.length) {
376
- const step = CW / labels.length;
377
- labels.forEach((lb, i) => {
378
- txt(slide, lb, { x: MX + step * i, y: 4.85, w: step, h: 0.3, fontSize: 10, color: ink2(), align: 'center', fontFace: mono() });
379
- });
380
- }
381
- footer(slide, p.footnoteLeft, p.footnoteRight);
382
- }
383
- // =====================================================================
384
- // 14. team
385
- // =====================================================================
386
- function renderTheme08Team(slide, p) {
387
- bg(slide);
388
- kicker(slide, p.kicker);
389
- title(slide, p.title);
390
- subtitle(slide, p.subtitle);
391
- const items = (p.items || []).slice(0, 4);
392
- const n = cols(items.length, 4);
393
- const gap = 0.3;
394
- const cw = (CW - gap * (n - 1)) / n;
395
- const ch = 2.5;
396
- items.forEach((m, i) => {
397
- const x = MX + i * (cw + gap);
398
- card(slide, x, BODY_TOP, cw, ch);
399
- const init = m.initial || (m.name ? m.name[0] : '?');
400
- slide.addShape('ellipse', { x: x + cw / 2 - 0.45, y: BODY_TOP + 0.25, w: 0.9, h: 0.9, fill: { color: gold() } });
401
- txt(slide, init, { x: x + cw / 2 - 0.45, y: BODY_TOP + 0.25, w: 0.9, h: 0.9, fontSize: 30, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', valign: 'middle', bold: true, fontFace: heading() });
402
- txt(slide, m.name, { x: x + 0.1, y: BODY_TOP + 1.3, w: cw - 0.2, h: 0.4, fontSize: 15, color: ink(), bold: true, align: 'center', fontFace: heading() });
403
- txt(slide, m.role, { x: x + 0.1, y: BODY_TOP + 1.75, w: cw - 0.2, h: 0.6, fontSize: 11.5, color: ink2(), align: 'center', valign: 'top', fontFace: body() });
404
- });
405
- footer(slide, p.footnoteLeft, p.footnoteRight);
406
- }
407
- // =====================================================================
408
- // 15. partners
409
- // =====================================================================
410
- function renderTheme08Partners(slide, p) {
411
- bg(slide);
412
- kicker(slide, p.kicker);
413
- title(slide, p.title);
414
- subtitle(slide, p.subtitle);
415
- const items = (p.items || []).slice(0, 12);
416
- const n = cols(items.length, 4);
417
- const rows = Math.ceil(items.length / n);
418
- const gap = 0.3;
419
- const cw = (CW - gap * (n - 1)) / n;
420
- const ch = Math.min(1.0, (5.0 - BODY_TOP) / rows - 0.15);
421
- items.forEach((it, i) => {
422
- const r = Math.floor(i / n);
423
- const c = i % n;
424
- const x = MX + c * (cw + gap);
425
- const y = BODY_TOP + r * (ch + 0.15);
426
- card(slide, x, y, cw, ch);
427
- txt(slide, it.name, { x: x + 0.15, y, w: cw - 0.3, h: ch, fontSize: 14, color: ink(), align: 'center', valign: 'middle', bold: true, fontFace: heading() });
428
- });
429
- footer(slide, p.footnoteLeft, p.footnoteRight);
430
- }
431
- // =====================================================================
432
- // 16. gallery
433
- // =====================================================================
434
- function renderTheme08Gallery(slide, p) {
435
- bg(slide);
436
- kicker(slide, p.kicker);
437
- title(slide, p.title);
438
- subtitle(slide, p.subtitle);
439
- const items = (p.items || []).slice(0, 4);
440
- const n = cols(items.length, 2);
441
- const gap = 0.3;
442
- const cw = (CW - gap * (n - 1)) / n;
443
- const ch = 1.55;
444
- items.forEach((it, i) => {
445
- const r = Math.floor(i / n);
446
- const c = i % n;
447
- const x = MX + c * (cw + gap);
448
- const y = BODY_TOP + r * (ch + 0.3);
449
- const ih = ch - 0.35;
450
- if (it.imageUrl) {
451
- try {
452
- slide.addImage({ path: it.imageUrl, x, y, w: cw, h: ih });
453
- }
454
- catch {
455
- slide.addShape('rect', { x, y, w: cw, h: ih, fill: { color: surfaceElev() }, line: { color: border(), width: 1 } });
456
- }
457
- }
458
- else {
459
- slide.addShape('roundRect', { x, y, w: cw, h: ih, rectRadius: 0.05, fill: { color: surfaceElev() }, line: { color: gold(), width: 1 } });
460
- txt(slide, 'IMG', { x, y, w: cw, h: ih, fontSize: 14, color: gold(), align: 'center', valign: 'middle', fontFace: mono() });
461
- }
462
- txt(slide, it.caption, { x, y: y + ih + 0.05, w: cw, h: 0.3, fontSize: 12, color: ink2(), align: 'left', fontFace: body() });
463
- });
464
- footer(slide, p.footnoteLeft, p.footnoteRight);
465
- }
466
- // =====================================================================
467
- // 17. collage
468
- // =====================================================================
469
- function renderTheme08Collage(slide, p) {
470
- bg(slide);
471
- if (p.tag)
472
- txt(slide, p.tag, { x: MX, y: 0.45, w: CW, h: 0.35, fontSize: 13, color: gold(), align: 'left', fontFace: mono(), bold: true, charSpacing: 2 });
473
- title(slide, p.title, MX, 0.82, CW, 28);
474
- txt(slide, p.subtitle, { x: MX, y: 1.5, w: 5.2, h: 0.8, fontSize: 14, color: ink2(), align: 'left', fontFace: body() });
475
- txt(slide, p.desc, { x: MX, y: 2.1, w: 5.2, h: 1.6, fontSize: 13, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
476
- const photos = (p.photos || []).slice(0, 4);
477
- const gx = 6.1;
478
- const gw = 3.2;
479
- const positions = [
480
- { x: gx, y: BODY_TOP, w: gw, h: 1.3 },
481
- { x: gx, y: BODY_TOP + 1.45, w: gw / 2 - 0.1, h: 1.3 },
482
- { x: gx + gw / 2 + 0.1, y: BODY_TOP + 1.45, w: gw / 2 - 0.1, h: 1.3 },
483
- { x: gx, y: BODY_TOP + 2.9, w: gw, h: 1.3 },
484
- ];
485
- photos.forEach((ph, i) => {
486
- const pos = positions[i];
487
- if (!pos)
488
- return;
489
- if (ph.imageUrl) {
490
- try {
491
- slide.addImage({ path: ph.imageUrl, x: pos.x, y: pos.y, w: pos.w, h: pos.h });
492
- }
493
- catch {
494
- slide.addShape('rect', { x: pos.x, y: pos.y, w: pos.w, h: pos.h, fill: { color: surfaceElev() }, line: { color: border(), width: 1 } });
495
- }
496
- }
497
- else {
498
- slide.addShape('roundRect', { x: pos.x, y: pos.y, w: pos.w, h: pos.h, rectRadius: 0.05, fill: { color: surfaceElev() }, line: { color: (ph.labelColor || gold()), width: 1 } });
499
- }
500
- if (ph.label)
501
- txt(slide, ph.label, { x: pos.x + 0.1, y: pos.y + pos.h - 0.35, w: pos.w - 0.2, h: 0.3, fontSize: 10, color: ph.labelColor || gold(), align: 'left', bold: true, fontFace: mono() });
502
- });
503
- if (p.bigNumber) {
504
- txt(slide, p.bigNumber, { x: gx, y: 5.0, w: gw, h: 0.6, fontSize: 30, color: gold(), align: 'right', bold: true, fontFace: heading() });
505
- if (p.bigNumberLabel)
506
- txt(slide, p.bigNumberLabel, { x: gx, y: 5.55, w: gw, h: 0.25, fontSize: 10, color: ink2(), align: 'right', fontFace: mono() });
507
- }
508
- footer(slide, p.footnoteLeft, p.footnoteRight);
509
- }
510
- // =====================================================================
511
- // 18. bubble
512
- // =====================================================================
513
- function renderTheme08Bubble(slide, p) {
514
- bg(slide);
515
- kicker(slide, p.kicker);
516
- title(slide, p.title);
517
- subtitle(slide, p.subtitle);
518
- const groups = (p.groups || []).slice(0, 3);
519
- const n = groups.length;
520
- const gap = 0.3;
521
- const cw = (CW - gap * (n - 1)) / n;
522
- groups.forEach((g, i) => {
523
- const x = MX + i * (cw + gap);
524
- card(slide, x, BODY_TOP, cw, 2.7);
525
- txt(slide, g.label, { x: x + 0.2, y: BODY_TOP + 0.2, w: cw - 0.4, h: 0.4, fontSize: 15, color: ink(), bold: true, align: 'left', fontFace: heading() });
526
- txt(slide, [g.count, g.total].filter(Boolean).join(' / '), { x: x + 0.2, y: BODY_TOP + 0.6, w: cw - 0.4, h: 0.5, fontSize: 24, color: gold(), align: 'left', bold: true, fontFace: heading() });
527
- (g.items || []).slice(0, 4).forEach((it, j) => {
528
- const label = typeof it === 'string' ? it : (it.label || it.name || '');
529
- txt(slide, '· ' + label, { x: x + 0.2, y: BODY_TOP + 1.2 + j * 0.36, w: cw - 0.4, h: 0.34, fontSize: 11, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
530
- });
531
- });
532
- footer(slide, p.footnoteLeft, p.footnoteRight);
533
- }
534
- // =====================================================================
535
- // 19. quote
536
- // =====================================================================
537
- function renderTheme08Quote(slide, p) {
538
- bg(slide);
539
- if (p.kicker)
540
- txt(slide, p.kicker, { x: MX, y: 1.0, w: CW, h: 0.35, fontSize: 13, color: gold(), align: 'center', fontFace: mono(), bold: true, charSpacing: 2 });
541
- txt(slide, '“', { x: MX, y: 1.2, w: 2, h: 1.2, fontSize: 90, color: gold(), align: 'left', fontFace: heading() });
542
- txt(slide, p.text, { x: 1.6, y: 1.7, w: CW - 1.8, h: 2.2, fontSize: 26, color: ink(), align: 'left', valign: 'top', bold: true, fontFace: heading() });
543
- if (p.author)
544
- txt(slide, '— ' + p.author, { x: 1.6, y: 4.0, w: CW - 1.8, h: 0.4, fontSize: 15, color: ink2(), align: 'left', fontFace: body() });
545
- footer(slide, p.footnoteLeft, p.footnoteRight);
546
- }
547
- // =====================================================================
548
- // 20. closing
549
- // =====================================================================
550
- function renderTheme08Closing(slide, p) {
551
- bg(slide);
552
- kicker(slide, p.kicker);
553
- txt(slide, p.title, { x: MX, y: 1.6, w: CW, h: 1.2, fontSize: 44, color: ink(), align: 'left', valign: 'middle', bold: true, fontFace: heading() });
554
- txt(slide, p.subtitle, { x: MX, y: 2.9, w: CW, h: 0.7, fontSize: 17, color: ink2(), align: 'left', fontFace: body() });
555
- const tags = (p.tags || []).slice(0, 5);
556
- let tx = MX;
557
- tags.forEach((t) => {
558
- const w = 0.4 + t.length * 0.18;
559
- slide.addShape('roundRect', { x: tx, y: 3.8, w, h: 0.45, rectRadius: 0.22, fill: { color: surfaceElev() }, line: { color: gold(), width: 1 } });
560
- txt(slide, t, { x: tx, y: 3.8, w, h: 0.45, fontSize: 12, color: gold(), align: 'center', valign: 'middle', fontFace: mono() });
561
- tx += w + 0.2;
562
- });
563
- if (p.contact)
564
- txt(slide, p.contact, { x: MX, y: 4.6, w: CW, h: 0.4, fontSize: 14, color: ink(), align: 'left', fontFace: body() });
565
- footer(slide, p.footnoteLeft, p.footnoteRight);
566
- }
567
- // =====================================================================
568
- // 21. hero-split
569
- // =====================================================================
570
- function renderTheme08HeroSplit(slide, p) {
571
- bg(slide);
572
- const halfW = CW / 2 - 0.2;
573
- slide.addShape('line', { x: 5, y: BODY_TOP - 0.2, w: 0, h: 2.9, line: { color: gold(), width: 1.5 } });
574
- // left
575
- if (p.leftKicker)
576
- txt(slide, p.leftKicker, { x: MX, y: BODY_TOP, w: halfW, h: 0.35, fontSize: 13, color: gold(), align: 'left', fontFace: mono(), bold: true });
577
- txt(slide, p.leftTitle, { x: MX, y: BODY_TOP + 0.45, w: halfW, h: 1.0, fontSize: 26, color: ink(), align: 'left', valign: 'top', bold: true, fontFace: heading() });
578
- txt(slide, p.leftDesc, { x: MX, y: BODY_TOP + 1.5, w: halfW, h: 1.5, fontSize: 14, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
579
- // right
580
- const rx = 5.2;
581
- if (p.rightKicker)
582
- txt(slide, p.rightKicker, { x: rx, y: BODY_TOP, w: halfW, h: 0.35, fontSize: 13, color: gold(), align: 'left', fontFace: mono(), bold: true });
583
- txt(slide, p.rightTitle, { x: rx, y: BODY_TOP + 0.45, w: halfW, h: 1.0, fontSize: 26, color: ink(), align: 'left', valign: 'top', bold: true, fontFace: heading() });
584
- txt(slide, p.rightDesc, { x: rx, y: BODY_TOP + 1.5, w: halfW, h: 1.5, fontSize: 14, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
585
- if (p.watermarkNumber)
586
- txt(slide, p.watermarkNumber, { x: MX, y: 4.7, w: CW, h: 0.6, fontSize: 40, color: gold(), align: 'center', bold: true, fontFace: heading() });
587
- footer(slide, p.footnoteLeft, p.footnoteRight);
588
- }
589
- // =====================================================================
590
- // 22. ecosystem
591
- // =====================================================================
592
- function renderTheme08Ecosystem(slide, p) {
593
- bg(slide);
594
- kicker(slide, p.kicker);
595
- title(slide, p.title, MX, 0.82, 5, 26);
596
- // hub
597
- const hx = 3.6, hy = 3.0, hr = 1.3;
598
- slide.addShape('ellipse', { x: hx, y: hy - hr / 2, w: hr, h: hr, fill: { color: gold() } });
599
- txt(slide, p.hubName, { x: hx, y: hy - hr / 2 + 0.15, w: hr, h: 0.4, fontSize: 14, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', bold: true, fontFace: heading() });
600
- txt(slide, [p.hubValue, p.hubUnit].filter(Boolean).join(' '), { x: hx, y: hy - hr / 2 + 0.5, w: hr, h: 0.5, fontSize: 20, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', bold: true, fontFace: heading() });
601
- if (p.hubSub)
602
- txt(slide, p.hubSub, { x: hx, y: hy - hr / 2 + 0.95, w: hr, h: 0.3, fontSize: 9, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', fontFace: mono() });
603
- const nodes = (p.nodes || []).slice(0, 6);
604
- const posMap = {
605
- top: [4.0, 1.7], right: [7.3, 2.8], bottom: [4.0, 4.3], left: [0.9, 2.8],
606
- };
607
- const order = { top: 0, right: 1, bottom: 2, left: 3 };
608
- nodes.sort((a, b) => (order[a.position] ?? 9) - (order[b.position] ?? 9));
609
- nodes.forEach((nd) => {
610
- const base = posMap[nd.position] || [4.0, 3.0];
611
- const nw = 2.2, nh = 0.9;
612
- card(slide, base[0], base[1], nw, nh);
613
- txt(slide, nd.name, { x: base[0] + 0.15, y: base[1] + 0.1, w: nw - 0.3, h: 0.35, fontSize: 13, color: ink(), bold: true, align: 'left', fontFace: heading() });
614
- txt(slide, [nd.value, nd.unit].filter(Boolean).join(' '), { x: base[0] + 0.15, y: base[1] + 0.45, w: nw - 0.3, h: 0.35, fontSize: 15, color: gold(), align: 'left', bold: true, fontFace: heading() });
615
- });
616
- footer(slide, p.footnoteLeft, p.footnoteRight);
617
- }
618
- // =====================================================================
619
- // 23. case
620
- // =====================================================================
621
- function renderTheme08Case(slide, p) {
622
- bg(slide);
623
- kicker(slide, p.kicker);
624
- title(slide, p.name, MX, 0.95, 6, 30);
625
- if (p.logoText) {
626
- slide.addShape('roundRect', { x: 7.2, y: 0.95, w: 1.6, h: 0.7, rectRadius: 0.1, fill: { color: gold() } });
627
- txt(slide, p.logoText, { x: 7.2, y: 0.95, w: 1.6, h: 0.7, fontSize: 16, color: isDark() ? '14130D' : 'FFFFFF', align: 'center', valign: 'middle', bold: true, fontFace: heading() });
628
- }
629
- if (p.tag)
630
- txt(slide, p.tag, { x: MX, y: 1.6, w: CW, h: 0.35, fontSize: 13, color: gold(), align: 'left', fontFace: mono(), bold: true });
631
- txt(slide, p.desc, { x: MX, y: 2.0, w: CW, h: 1.2, fontSize: 14, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
632
- const metrics = (p.metrics || []).slice(0, 3);
633
- if (metrics.length) {
634
- const mw = CW / metrics.length;
635
- metrics.forEach((m, i) => {
636
- const x = MX + i * mw;
637
- txt(slide, [m.num].filter(Boolean).join(''), { x, y: 3.6, w: mw, h: 0.6, fontSize: 28, color: gold(), align: 'left', bold: true, fontFace: heading() });
638
- txt(slide, m.label, { x, y: 4.2, w: mw, h: 0.4, fontSize: 12, color: ink2(), align: 'left', fontFace: body() });
639
- });
640
- }
641
- footer(slide, p.footnoteLeft, p.footnoteRight);
642
- }
643
- // =====================================================================
644
- // 24. funding
645
- // =====================================================================
646
- function renderTheme08Funding(slide, p) {
647
- bg(slide);
648
- kicker(slide, p.kicker);
649
- title(slide, p.title);
650
- subtitle(slide, p.subtitle);
651
- const rows = (p.rows || []).slice(0, 5);
652
- let y = BODY_TOP;
653
- const rowH = (5.0 - BODY_TOP) / Math.max(rows.length, 1);
654
- rows.forEach((r) => {
655
- card(slide, MX, y, CW, rowH - 0.12, r.alt ? surfaceElev() : undefined);
656
- txt(slide, r.name, { x: MX + 0.2, y: y + 0.05, w: 2.4, h: rowH - 0.2, fontSize: 15, color: ink(), bold: true, align: 'left', valign: 'middle', fontFace: heading() });
657
- txt(slide, r.nameSub, { x: MX + 0.2, y: y + rowH / 2, w: 2.4, h: rowH / 2, fontSize: 10, color: ink2(), align: 'left', fontFace: body() });
658
- // dots
659
- const dotN = Math.min(r.dotCount || 0, 10);
660
- let dx = MX + 2.7;
661
- for (let k = 0; k < dotN; k++) {
662
- slide.addShape('ellipse', { x: dx, y: y + rowH / 2 - 0.05, w: 0.1, h: 0.1, fill: { color: gold() } });
663
- dx += 0.16;
664
- }
665
- txt(slide, r.count, { x: MX + 4.6, y, w: 1.2, h: rowH - 0.12, fontSize: 18, color: gold(), align: 'left', valign: 'middle', bold: true, fontFace: heading() });
666
- txt(slide, '均值 ' + (r.avg || ''), { x: MX + 5.8, y, w: 1.6, h: rowH - 0.12, fontSize: 11, color: ink2(), align: 'left', valign: 'middle', fontFace: body() });
667
- txt(slide, '信号 ' + (r.signalCount || 0), { x: MX + 7.4, y, w: 1.4, h: rowH - 0.12, fontSize: 11, color: ink2(), align: 'right', valign: 'middle', fontFace: body() });
668
- y += rowH;
669
- });
670
- if (p.summaryPct) {
671
- txt(slide, [p.summaryLabel, p.summaryPct].filter(Boolean).join(' '), { x: MX, y: 5.05, w: CW, h: 0.3, fontSize: 12, color: gold(), align: 'left', bold: true, fontFace: heading() });
672
- }
673
- footer(slide, p.footnoteLeft, p.footnoteRight);
674
- }
675
- // =====================================================================
676
- // 25. matrix(2x2)
677
- // =====================================================================
678
- function renderTheme08Matrix(slide, p) {
679
- bg(slide);
680
- kicker(slide, p.kicker);
681
- title(slide, p.title);
682
- subtitle(slide, p.subtitle);
683
- const cells = (p.cells || []).slice(0, 4);
684
- const gap = 0.25;
685
- const cw = (CW - gap) / 2;
686
- const ch = 1.45;
687
- const positions = [
688
- { x: MX, y: BODY_TOP },
689
- { x: MX + cw + gap, y: BODY_TOP },
690
- { x: MX, y: BODY_TOP + ch + gap },
691
- { x: MX + cw + gap, y: BODY_TOP + ch + gap },
692
- ];
693
- cells.forEach((c, i) => {
694
- const pos = positions[i];
695
- const fill = c.dark ? gold() : surfaceElev();
696
- const tcol = c.dark ? (isDark() ? '14130D' : 'FFFFFF') : ink();
697
- card(slide, pos.x, pos.y, cw, ch, fill);
698
- txt(slide, c.title, { x: pos.x + 0.2, y: pos.y + 0.15, w: cw - 0.4, h: 0.4, fontSize: 15, color: tcol, bold: true, align: 'left', fontFace: heading() });
699
- txt(slide, c.desc, { x: pos.x + 0.2, y: pos.y + 0.55, w: cw - 0.4, h: ch - 0.7, fontSize: 11, color: tcol, align: 'left', valign: 'top', fontFace: body() });
700
- if (c.cornerBadge)
701
- txt(slide, c.cornerBadge, { x: pos.x + cw - 1.0, y: pos.y + 0.1, w: 0.9, h: 0.3, fontSize: 10, color: tcol, align: 'right', bold: true, fontFace: mono() });
702
- });
703
- if (p.xAxisLabel || p.yAxisLabel) {
704
- txt(slide, [p.xAxisLabel, p.yAxisLabel].filter(Boolean).join(' · '), { x: MX, y: 5.05, w: CW, h: 0.3, fontSize: 10, color: ink2(), align: 'left', fontFace: mono() });
705
- }
706
- footer(slide, p.footnoteLeft, p.footnoteRight);
707
- }
708
- // =====================================================================
709
- // 26. quadrant(2x2)
710
- // =====================================================================
711
- function renderTheme08Quadrant(slide, p) {
712
- bg(slide);
713
- kicker(slide, p.kicker);
714
- title(slide, p.title);
715
- subtitle(slide, p.subtitle);
716
- const items = (p.items || []).slice(0, 4);
717
- const gap = 0.25;
718
- const cw = (CW - gap) / 2;
719
- const ch = 1.45;
720
- const positions = [
721
- { x: MX, y: BODY_TOP },
722
- { x: MX + cw + gap, y: BODY_TOP },
723
- { x: MX, y: BODY_TOP + ch + gap },
724
- { x: MX + cw + gap, y: BODY_TOP + ch + gap },
725
- ];
726
- items.forEach((it, i) => {
727
- const pos = positions[i];
728
- card(slide, pos.x, pos.y, cw, ch);
729
- if (it.label)
730
- txt(slide, it.label, { x: pos.x + 0.2, y: pos.y + 0.12, w: cw - 0.4, h: 0.3, fontSize: 11, color: gold(), align: 'left', bold: true, fontFace: mono() });
731
- txt(slide, it.title, { x: pos.x + 0.2, y: pos.y + 0.42, w: cw - 0.4, h: 0.4, fontSize: 15, color: ink(), bold: true, align: 'left', fontFace: heading() });
732
- txt(slide, it.desc, { x: pos.x + 0.2, y: pos.y + 0.82, w: cw - 0.4, h: ch - 0.95, fontSize: 11, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
733
- });
734
- footer(slide, p.footnoteLeft, p.footnoteRight);
735
- }
736
- // =====================================================================
737
- // 27. scorecard
738
- // =====================================================================
739
- function renderTheme08Scorecard(slide, p) {
740
- bg(slide);
741
- kicker(slide, p.kicker);
742
- title(slide, p.title);
743
- subtitle(slide, p.subtitle);
744
- const cards = (p.cards || []).slice(0, 4);
745
- const n = cards.length;
746
- const gap = 0.3;
747
- const cw = (CW - gap * (n - 1)) / n;
748
- cards.forEach((c, i) => {
749
- const x = MX + i * (cw + gap);
750
- card(slide, x, BODY_TOP, cw, 2.7, c.highlight ? gold() : undefined);
751
- const tcol = c.highlight ? (isDark() ? '14130D' : 'FFFFFF') : ink();
752
- txt(slide, c.label, { x: x + 0.2, y: BODY_TOP + 0.2, w: cw - 0.4, h: 0.35, fontSize: 13, color: tcol, align: 'left', fontFace: body() });
753
- txt(slide, c.value, { x: x + 0.2, y: BODY_TOP + 0.55, w: cw - 0.4, h: 0.7, fontSize: 30, color: tcol, align: 'left', bold: true, fontFace: heading() });
754
- if (c.change) {
755
- const up = c.changeDir !== 'down';
756
- slide.addShape('roundRect', { x: x + 0.2, y: BODY_TOP + 1.35, w: 1.0, h: 0.35, rectRadius: 0.17, fill: { color: up ? (isDark() ? '1A1812' : 'E8F5E9') : (isDark() ? '2C1414' : 'FDECEA') } });
757
- txt(slide, (up ? '▲ ' : '▼ ') + c.change, { x: x + 0.2, y: BODY_TOP + 1.35, w: 1.0, h: 0.35, fontSize: 11, color: up ? (isDark() ? '3DDC84' : '1B7A3D') : (isDark() ? 'FF6B6B' : 'C0392B'), align: 'center', valign: 'middle', bold: true, fontFace: mono() });
758
- }
759
- if (c.desc)
760
- txt(slide, c.desc, { x: x + 0.2, y: BODY_TOP + 1.85, w: cw - 0.4, h: 0.8, fontSize: 11, color: tcol, align: 'left', valign: 'top', fontFace: body() });
761
- });
762
- footer(slide, p.footnoteLeft, p.footnoteRight);
763
- }
764
- // =====================================================================
765
- // 28. region
766
- // =====================================================================
767
- function renderTheme08Region(slide, p) {
768
- bg(slide);
769
- kicker(slide, p.kicker);
770
- title(slide, p.title);
771
- subtitle(slide, p.subtitle);
772
- const regions = (p.regions || []).slice(0, 7);
773
- let y = BODY_TOP;
774
- const rowH = (5.0 - BODY_TOP) / Math.max(regions.length, 1);
775
- // find max for bar scaling
776
- const maxV = Math.max(...regions.map((r) => parseFloat(r.value) || 0), 1);
777
- regions.forEach((r) => {
778
- txt(slide, r.city, { x: MX, y, w: 2.2, h: rowH - 0.1, fontSize: 14, color: ink(), bold: true, align: 'left', valign: 'middle', fontFace: heading() });
779
- const v = parseFloat(r.value) || 0;
780
- const bw = (CW - 2.4) * (v / maxV);
781
- slide.addShape('roundRect', { x: MX + 2.3, y: y + (rowH - 0.1) / 2 - 0.12, w: Math.max(bw, 0.1), h: 0.24, rectRadius: 0.12, fill: { color: gold() } });
782
- txt(slide, r.value, { x: MX + 2.3 + Math.max(bw, 0.1) + 0.1, y, w: 1.5, h: rowH - 0.1, fontSize: 13, color: ink2(), align: 'left', valign: 'middle', fontFace: mono() });
783
- y += rowH;
784
- });
785
- footer(slide, p.footnoteLeft, p.footnoteRight);
786
- }
787
- // =====================================================================
788
- // 29. range
789
- // =====================================================================
790
- function renderTheme08Range(slide, p) {
791
- bg(slide);
792
- kicker(slide, p.kicker);
793
- title(slide, p.title);
794
- subtitle(slide, p.subtitle);
795
- const rows = (p.rows || []).slice(0, 5);
796
- let y = BODY_TOP;
797
- const rowH = (5.0 - BODY_TOP) / Math.max(rows.length, 1);
798
- const maxHigh = Math.max(...rows.map((r) => r.high || 0), 1);
799
- rows.forEach((r) => {
800
- txt(slide, r.name, { x: MX, y, w: 2.0, h: rowH - 0.1, fontSize: 13, color: ink(), bold: true, align: 'left', valign: 'middle', fontFace: heading() });
801
- txt(slide, r.nameSub, { x: MX, y: y + rowH / 2, w: 2.0, h: rowH / 2, fontSize: 9, color: ink2(), align: 'left', fontFace: body() });
802
- const trackX = MX + 2.2;
803
- const trackW = CW - 2.2 - 1.6;
804
- slide.addShape('roundRect', { x: trackX, y: y + (rowH - 0.1) / 2 - 0.09, w: trackW, h: 0.18, rectRadius: 0.09, fill: { color: surfaceElev() } });
805
- const lowX = trackX + trackW * (r.low / maxHigh);
806
- const highX = trackX + trackW * (r.high / maxHigh);
807
- slide.addShape('roundRect', { x: lowX, y: y + (rowH - 0.1) / 2 - 0.11, w: Math.max(highX - lowX, 0.1), h: 0.22, rectRadius: 0.11, fill: { color: r.highlight ? gold() : (isDark() ? 'B8AE98' : '524C3D') } });
808
- txt(slide, [r.lowVal, r.highVal].filter(Boolean).join(' – '), { x: MX + CW - 1.5, y, w: 1.5, h: rowH - 0.1, fontSize: 11, color: ink2(), align: 'right', valign: 'middle', fontFace: mono() });
809
- y += rowH;
810
- });
811
- if (p.midLabel)
812
- txt(slide, p.midLabel, { x: MX, y: 5.05, w: CW, h: 0.3, fontSize: 10, color: ink2(), align: 'left', fontFace: mono() });
813
- footer(slide, p.footnoteLeft, p.footnoteRight);
814
- }
815
- // =====================================================================
816
- // 30. heatmap
817
- // =====================================================================
818
- function renderTheme08Heatmap(slide, p) {
819
- bg(slide);
820
- kicker(slide, p.kicker);
821
- title(slide, p.title);
822
- subtitle(slide, p.subtitle);
823
- const cells = (p.cells || []).slice(0, 12);
824
- const n = cols(cells.length, 6);
825
- const rowsN = Math.ceil(cells.length / n);
826
- const gap = 0.15;
827
- const cw = (CW - gap * (n - 1)) / n;
828
- const ch = Math.min(0.95, (5.0 - BODY_TOP) / rowsN - gap);
829
- cells.forEach((c, i) => {
830
- const r = Math.floor(i / n);
831
- const col = i % n;
832
- const x = MX + col * (cw + gap);
833
- const y = BODY_TOP + r * (ch + gap);
834
- slide.addShape('roundRect', { x, y, w: cw, h: ch, rectRadius: 0.05, fill: { color: c.color || gold() } });
835
- txt(slide, c.month, { x: x + 0.05, y: y + 0.02, w: cw - 0.1, h: 0.3, fontSize: 9, color: isDark() ? '14130D' : '1A1812', align: 'left', bold: true, fontFace: mono() });
836
- txt(slide, c.value + (c.unit || ''), { x: x + 0.05, y: y + ch - 0.4, w: cw - 0.1, h: 0.35, fontSize: 12, color: isDark() ? '14130D' : '1A1812', align: 'left', bold: true, fontFace: heading() });
837
- });
838
- if (p.scaleMin || p.scaleMax) {
839
- txt(slide, [p.scaleMin, p.scaleMax, p.scaleUnit].filter(Boolean).join(' '), { x: MX, y: 5.05, w: CW, h: 0.3, fontSize: 10, color: ink2(), align: 'left', fontFace: mono() });
840
- }
841
- footer(slide, p.footnoteLeft, p.footnoteRight);
842
- }
843
- // =====================================================================
844
- // 31. waterfall
845
- // =====================================================================
846
- function renderTheme08Waterfall(slide, p) {
847
- bg(slide);
848
- kicker(slide, p.kicker);
849
- title(slide, p.title);
850
- subtitle(slide, p.subtitle);
851
- const bars = (p.bars || []).slice(0, 7);
852
- const n = bars.length;
853
- const gap = 0.25;
854
- const cw = (CW - gap * (n - 1)) / n;
855
- const baseY = 4.6;
856
- const maxV = Math.max(...bars.map((b) => Math.abs(b.value)), 1);
857
- const scale = (baseY - BODY_TOP - 0.3) / maxV;
858
- let cum = 0;
859
- bars.forEach((b, i) => {
860
- const x = MX + i * (cw + gap);
861
- const startY = baseY - cum * scale;
862
- const h = Math.abs(b.value) * scale;
863
- const top = b.value >= 0 ? startY - h : startY;
864
- slide.addShape('roundRect', { x, y: top, w: cw, h: Math.max(h, 0.05), rectRadius: 0.03, fill: { color: b.color || (b.value >= 0 ? gold() : (isDark() ? 'FF6B6B' : 'C0392B')) } });
865
- txt(slide, b.value + (b.pct || ''), { x, y: top - 0.35, w: cw, h: 0.3, fontSize: 12, color: ink(), align: 'center', bold: true, fontFace: heading() });
866
- txt(slide, b.label, { x, y: baseY + 0.05, w: cw, h: 0.3, fontSize: 10, color: ink2(), align: 'center', fontFace: body() });
867
- if (b.sub)
868
- txt(slide, b.sub, { x, y: baseY + 0.33, w: cw, h: 0.25, fontSize: 8.5, color: ink2(), align: 'center', fontFace: mono() });
869
- cum += b.value;
870
- });
871
- if (p.totalValue) {
872
- txt(slide, [p.totalLabel, p.totalValue].filter(Boolean).join(':'), { x: MX, y: 5.05, w: CW, h: 0.3, fontSize: 12, color: gold(), align: 'left', bold: true, fontFace: heading() });
873
- }
874
- footer(slide, p.footnoteLeft, p.footnoteRight);
875
- }
876
- // =====================================================================
877
- // 32. chart-bar
878
- // =====================================================================
879
- function renderTheme08ChartBar(slide, p) {
880
- bg(slide);
881
- kicker(slide, p.kicker);
882
- title(slide, p.title);
883
- subtitle(slide, p.subtitle);
884
- const bars = (p.bars || []).slice(0, 8);
885
- if (bars.length) {
886
- slide.addChart('bar', [{ name: p.title || '数据', labels: bars.map((b) => b.label), values: bars.map((b) => parseFloat(b.value) || 0) }], {
887
- x: MX, y: BODY_TOP, w: CW, h: 2.9,
888
- chartColors: [gold()],
889
- barDir: 'col',
890
- showValue: true,
891
- dataLabelColor: ink(),
892
- dataLabelFontSize: 10,
893
- catAxisLabelColor: ink2(),
894
- valAxisLabelColor: ink2(),
895
- catAxisLabelFontFace: body(),
896
- valAxisLabelFontFace: body(),
897
- });
898
- }
899
- footer(slide, p.footnoteLeft, p.footnoteRight);
900
- }
901
- // =====================================================================
902
- // 33. chart-donut
903
- // =====================================================================
904
- function renderTheme08ChartDonut(slide, p) {
905
- bg(slide);
906
- kicker(slide, p.kicker);
907
- title(slide, p.title);
908
- subtitle(slide, p.subtitle);
909
- const segs = (p.segments || []).slice(0, 8);
910
- if (segs.length) {
911
- slide.addChart('doughnut', [{ name: p.title || '占比', labels: segs.map((s) => s.name), values: segs.map((s) => parseFloat(s.value) || 0) }], {
912
- x: 0.8, y: BODY_TOP, w: 5.2, h: 2.9,
913
- chartColors: S.chartColors,
914
- holeSize: 60,
915
- showValue: false,
916
- showLegend: true,
917
- legendColor: ink2(),
918
- legendFontFace: body(),
919
- legendFontSize: 10,
920
- });
921
- if (p.centerNum) {
922
- txt(slide, p.centerNum, { x: 2.5, y: BODY_TOP + 1.0, w: 1.6, h: 0.6, fontSize: 24, color: gold(), align: 'center', valign: 'middle', bold: true, fontFace: heading() });
923
- if (p.centerLabel)
924
- txt(slide, p.centerLabel, { x: 2.5, y: BODY_TOP + 1.55, w: 1.6, h: 0.3, fontSize: 10, color: ink2(), align: 'center', fontFace: body() });
925
- }
926
- // legend values on right
927
- let ly = BODY_TOP + 0.2;
928
- segs.forEach((s) => {
929
- txt(slide, [s.name, s.value].filter(Boolean).join(' '), { x: 6.4, y: ly, w: 3, h: 0.35, fontSize: 12, color: ink(), align: 'left', fontFace: body() });
930
- ly += 0.45;
931
- });
932
- }
933
- footer(slide, p.footnoteLeft, p.footnoteRight);
934
- }
935
- // =====================================================================
936
- // 34. gauge
937
- // =====================================================================
938
- function renderTheme08Gauge(slide, p) {
939
- bg(slide);
940
- kicker(slide, p.kicker);
941
- title(slide, p.title);
942
- if (p.year)
943
- txt(slide, p.year, { x: MX, y: 1.5, w: CW, h: 0.35, fontSize: 13, color: gold(), align: 'left', fontFace: mono(), bold: true });
944
- // big value
945
- txt(slide, [p.value, p.unit].filter(Boolean).join(''), { x: MX, y: 2.0, w: 4.5, h: 1.4, fontSize: 80, color: gold(), align: 'left', valign: 'middle', bold: true, fontFace: heading() });
946
- if (p.desc)
947
- txt(slide, p.desc, { x: MX, y: 3.6, w: 4.5, h: 1.0, fontSize: 14, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
948
- // progress bar (value as percentage of 100)
949
- const pct = Math.max(0, Math.min(100, parseFloat(p.value) || 0));
950
- slide.addShape('roundRect', { x: 5.4, y: 2.4, w: 3.8, h: 0.3, rectRadius: 0.15, fill: { color: surfaceElev() } });
951
- slide.addShape('roundRect', { x: 5.4, y: 2.4, w: 3.8 * (pct / 100), h: 0.3, rectRadius: 0.15, fill: { color: gold() } });
952
- txt(slide, pct.toFixed(0) + '%', { x: 5.4, y: 2.75, w: 3.8, h: 0.3, fontSize: 12, color: ink2(), align: 'left', fontFace: mono() });
953
- const breakdown = (p.breakdown || []).slice(0, 5);
954
- let by = 3.3;
955
- breakdown.forEach((b) => {
956
- txt(slide, [b.label, b.value].filter(Boolean).join(' '), { x: 5.4, y: by, w: 3.8, h: 0.35, fontSize: 12, color: ink(), align: 'left', fontFace: body() });
957
- by += 0.42;
958
- });
959
- footer(slide, p.footnoteLeft, p.footnoteRight);
960
- }
961
- // =====================================================================
962
- // 35. radar
963
- // =====================================================================
964
- function renderTheme08Radar(slide, p) {
965
- bg(slide);
966
- kicker(slide, p.kicker);
967
- title(slide, p.title);
968
- subtitle(slide, p.subtitle);
969
- const axes = (p.axes || []).slice(0, 8);
970
- if (axes.length) {
971
- slide.addChart('radar', [{ name: p.title || '能力', labels: axes.map((a) => a.label), values: axes.map((a) => a.value || 0) }], {
972
- x: MX, y: BODY_TOP, w: 5.4, h: 2.9,
973
- chartColors: [gold()],
974
- showValue: false,
975
- catAxisLabelColor: ink2(),
976
- catAxisLabelFontFace: body(),
977
- catAxisLabelFontSize: 10,
978
- });
979
- }
980
- const metrics = (p.metrics || []).slice(0, 6);
981
- let my = BODY_TOP + 0.1;
982
- metrics.forEach((m) => {
983
- txt(slide, m.label, { x: 6.4, y: my, w: 3, h: 0.3, fontSize: 12, color: ink(), bold: true, align: 'left', fontFace: heading() });
984
- txt(slide, String(m.value), { x: 6.4, y: my + 0.3, w: 1.5, h: 0.3, fontSize: 14, color: gold(), align: 'left', bold: true, fontFace: heading() });
985
- if (m.delta)
986
- txt(slide, (m.delta > 0 ? '▲' : '▼') + ' ' + m.delta, { x: 8.0, y: my + 0.3, w: 1.4, h: 0.3, fontSize: 11, color: m.delta > 0 ? (isDark() ? '3DDC84' : '1B7A3D') : (isDark() ? 'FF6B6B' : 'C0392B'), align: 'left', fontFace: mono() });
987
- my += 0.45;
988
- });
989
- footer(slide, p.footnoteLeft, p.footnoteRight);
990
- }
991
- // =====================================================================
992
- // 36. ranking
993
- // =====================================================================
994
- function renderTheme08Ranking(slide, p) {
995
- bg(slide);
996
- kicker(slide, p.kicker);
997
- title(slide, p.title);
998
- subtitle(slide, p.subtitle);
999
- const items = (p.items || []).slice(0, 6);
1000
- const maxV = Math.max(...items.map((it) => parseFloat(it.value) || 0), 1);
1001
- let y = BODY_TOP;
1002
- const rowH = (5.0 - BODY_TOP) / Math.max(items.length, 1);
1003
- items.forEach((it, i) => {
1004
- const rank = String(i + 1).padStart(2, '0');
1005
- txt(slide, rank, { x: MX, y, w: 0.7, h: rowH - 0.1, fontSize: 18, color: it.accent ? gold() : ink2(), align: 'left', valign: 'middle', bold: true, fontFace: mono() });
1006
- txt(slide, it.name, { x: MX + 0.8, y, w: 3.2, h: rowH - 0.1, fontSize: 14, color: ink(), bold: true, align: 'left', valign: 'middle', fontFace: heading() });
1007
- const trackX = MX + 4.1;
1008
- const trackW = CW - 4.1 - 1.3;
1009
- const v = parseFloat(it.value) || 0;
1010
- slide.addShape('roundRect', { x: trackX, y: y + (rowH - 0.1) / 2 - 0.1, w: trackW, h: 0.2, rectRadius: 0.1, fill: { color: surfaceElev() } });
1011
- slide.addShape('roundRect', { x: trackX, y: y + (rowH - 0.1) / 2 - 0.1, w: Math.max(trackW * (v / maxV), 0.1), h: 0.2, rectRadius: 0.1, fill: { color: it.accent ? gold() : (isDark() ? 'B8AE98' : '524C3D') } });
1012
- txt(slide, (p.unit ? it.value + ' ' + p.unit : it.value), { x: MX + CW - 1.2, y, w: 1.2, h: rowH - 0.1, fontSize: 13, color: ink2(), align: 'right', valign: 'middle', fontFace: mono() });
1013
- y += rowH;
1014
- });
1015
- footer(slide, p.footnoteLeft, p.footnoteRight);
1016
- }
1017
- // =====================================================================
1018
- // 37. compare
1019
- // =====================================================================
1020
- function renderTheme08Compare(slide, p) {
1021
- bg(slide);
1022
- kicker(slide, p.kicker);
1023
- title(slide, p.title);
1024
- subtitle(slide, p.subtitle);
1025
- const items = (p.items || []).slice(0, 6);
1026
- const left = items.filter((it) => it.side !== 'right').slice(0, 3);
1027
- const right = items.filter((it) => it.side === 'right').slice(0, 3);
1028
- const halfW = CW / 2 - 0.2;
1029
- slide.addShape('line', { x: 5, y: BODY_TOP - 0.1, w: 0, h: 2.9, line: { color: border(), width: 1 } });
1030
- const drawCol = (arr, x) => {
1031
- let y = BODY_TOP;
1032
- const rh = 2.9 / Math.max(arr.length, 1);
1033
- arr.forEach((it) => {
1034
- txt(slide, it.title, { x, y, w: halfW - 0.2, h: 0.4, fontSize: 15, color: gold(), bold: true, align: 'left', fontFace: heading() });
1035
- txt(slide, it.desc, { x, y: y + 0.4, w: halfW - 0.2, h: rh - 0.45, fontSize: 12, color: ink2(), align: 'left', valign: 'top', fontFace: body() });
1036
- y += rh;
1037
- });
1038
- };
1039
- drawCol(left, MX);
1040
- drawCol(right, 5.2);
1041
- footer(slide, p.footnoteLeft, p.footnoteRight);
1042
- }
1043
- // =====================================================================
1044
- // 38. table
1045
- // =====================================================================
1046
- function renderTheme08Table(slide, p) {
1047
- bg(slide);
1048
- kicker(slide, p.kicker);
1049
- title(slide, p.title);
1050
- subtitle(slide, p.subtitle);
1051
- const headers = (p.headers || []).slice(0, 4);
1052
- const rows = (p.rows || []).slice(0, 8);
1053
- const colsN = Math.max(headers.length, 1);
1054
- const gap = 0.1;
1055
- const cw = (CW - gap * (colsN - 1)) / colsN;
1056
- const headY = BODY_TOP;
1057
- // header
1058
- headers.forEach((h, i) => {
1059
- const x = MX + i * (cw + gap);
1060
- slide.addShape('rect', { x, y: headY, w: cw, h: 0.45, fill: { color: gold() } });
1061
- txt(slide, h, { x: x + 0.15, y: headY, w: cw - 0.3, h: 0.45, fontSize: 13, color: isDark() ? '14130D' : 'FFFFFF', align: 'left', valign: 'middle', bold: true, fontFace: heading() });
1062
- });
1063
- const rowH = (5.0 - BODY_TOP - 0.55) / Math.max(rows.length, 1);
1064
- rows.forEach((r, ri) => {
1065
- const y = headY + 0.55 + ri * rowH;
1066
- if (ri % 2 === 1)
1067
- slide.addShape('rect', { x: MX, y, w: CW, h: rowH, fill: { color: surfaceElev() } });
1068
- const cellsArr = [r.c1, r.c2, r.c3, r.c4].slice(0, colsN);
1069
- cellsArr.forEach((c, i) => {
1070
- const x = MX + i * (cw + gap);
1071
- txt(slide, c, { x: x + 0.15, y, w: cw - 0.3, h: rowH, fontSize: 12, color: ink(), align: 'left', valign: 'middle', fontFace: body() });
1072
- });
1073
- });
1074
- footer(slide, p.footnoteLeft, p.footnoteRight);
1075
- }
1076
- // =====================================================================
1077
- // 39. chain
1078
- // =====================================================================
1079
- function renderTheme08Chain(slide, p) {
1080
- bg(slide);
1081
- kicker(slide, p.kicker);
1082
- title(slide, p.title);
1083
- subtitle(slide, p.subtitle);
1084
- const layers = (p.layers || []).slice(0, 5);
1085
- const n = layers.length;
1086
- const gap = 0.3;
1087
- const cw = (CW - gap * (n - 1)) / n;
1088
- layers.forEach((l, i) => {
1089
- const x = MX + i * (cw + gap);
1090
- const fill = l.dark ? gold() : surfaceElev();
1091
- const tcol = l.dark ? (isDark() ? '14130D' : 'FFFFFF') : ink();
1092
- card(slide, x, BODY_TOP, cw, 2.6, fill);
1093
- txt(slide, l.title, { x: x + 0.2, y: BODY_TOP + 0.25, w: cw - 0.4, h: 0.7, fontSize: 17, color: tcol, bold: true, align: 'left', valign: 'top', fontFace: heading() });
1094
- txt(slide, l.subtitle, { x: x + 0.2, y: BODY_TOP + 1.0, w: cw - 0.4, h: 1.4, fontSize: 12, color: tcol, align: 'left', valign: 'top', fontFace: body() });
1095
- if (i < n - 1) {
1096
- txt(slide, '→', { x: x + cw + 0.02, y: BODY_TOP + 1.0, w: gap, h: 0.5, fontSize: 18, color: gold(), align: 'center', valign: 'middle', bold: true });
1097
- }
1098
- });
1099
- footer(slide, p.footnoteLeft, p.footnoteRight);
1100
- }
1101
- // =====================================================================
1102
- // v2 变体版式家族渲染器(cover/chapter/quote/cards/bars/line/dumbbell/panels/statwall/arch/capital)
1103
- // =====================================================================
1104
- function foot(p) {
1105
- return [p.footnoteLeft || p.footerLeft, p.footnoteRight || p.footerRight];
1106
- }
1107
- function gridGeom(n, perRow, top = BODY_TOP, bottom = 5.0) {
1108
- const rows = Math.max(1, Math.ceil(n / perRow));
1109
- const gap = 0.18;
1110
- const cw = (CW - gap * (perRow - 1)) / perRow;
1111
- const ch = (bottom - top - gap * (rows - 1)) / rows;
1112
- return { rows, perRow, gap, cw, ch, top, bottom };
1113
- }
1114
- function renderT08Cover2(slide, p) {
1115
- bg(slide);
1116
- if (p.tag)
1117
- kicker(slide, p.tag);
1118
- title(slide, p.title, MX, 1.5, CW, 40);
1119
- subtitle(slide, p.subtitle);
1120
- const stats = (p.stats || []).slice(0, 4);
1121
- if (stats.length) {
1122
- const mw = CW / stats.length;
1123
- stats.forEach((m, i) => {
1124
- const x = MX + i * mw;
1125
- txt(slide, [m.value, m.unit].filter(Boolean).join(' '), { x, y: 3.7, w: mw, h: 0.5, fontSize: 24, color: m.accent ? gold() : ink(), bold: true, align: 'left', fontFace: heading() });
1126
- txt(slide, m.label, { x, y: 4.22, w: mw, h: 0.35, fontSize: 12, color: ink2(), align: 'left' });
1127
- });
1128
- }
1129
- footer(slide, ...foot(p));
1130
- }
1131
- function renderT08Chapter2(slide, p) {
1132
- bg(slide);
1133
- if (p.chapterNo)
1134
- txt(slide, p.chapterNo, { x: MX, y: 1.3, w: CW, h: 1.7, fontSize: 100, color: gold(), bold: true, align: 'left', fontFace: heading() });
1135
- title(slide, p.title, MX, 3.1, CW, 36);
1136
- subtitle(slide, p.subtitle);
1137
- footer(slide, ...foot(p));
1138
- }
1139
- function renderT08Quote2(slide, p) {
1140
- bg(slide);
1141
- kicker(slide, p.kicker);
1142
- txt(slide, p.quote, { x: MX, y: 1.5, w: CW, h: 2.4, fontSize: 28, color: ink(), bold: true, align: 'left', valign: 'top', fontFace: heading() });
1143
- if (p.fieldA)
1144
- txt(slide, p.fieldA, { x: MX, y: 3.9, w: CW, h: 0.6, fontSize: 16, color: ink2(), valign: 'top' });
1145
- if (p.cite)
1146
- txt(slide, p.cite, { x: MX, y: 4.7, w: CW, h: 0.4, fontSize: 13, color: ink2(), italic: true });
1147
- footer(slide, ...foot(p));
1148
- }
1149
- function renderT08GridCards(slide, p, opts) {
1150
- bg(slide);
1151
- kicker(slide, p.kicker);
1152
- title(slide, p.title);
1153
- subtitle(slide, p.subtitle);
1154
- const items = (p.items || []).slice(0, opts.list ? 6 : 12);
1155
- const perRow = opts.list ? 1 : Math.min(opts.cols, items.length || 1);
1156
- const g = gridGeom(items.length || 1, perRow);
1157
- items.forEach((it, i) => {
1158
- const r = Math.floor(i / perRow), c = i % perRow;
1159
- const x = MX + c * (g.cw + g.gap);
1160
- const y = g.top + r * (g.ch + g.gap);
1161
- card(slide, x, y, g.cw, g.ch);
1162
- let yy = y + 0.18;
1163
- if (it.num != null && it.num !== '') {
1164
- txt(slide, [it.num, it.unit].filter(Boolean).join(' '), { x: x + 0.2, y: yy, w: g.cw - 0.4, h: 0.4, fontSize: 22, color: gold(), bold: true, fontFace: heading() });
1165
- yy += 0.5;
1166
- }
1167
- if (it.title) {
1168
- txt(slide, it.title, { x: x + 0.2, y: yy, w: g.cw - 0.4, h: 0.4, fontSize: 15, color: ink(), bold: true, fontFace: heading() });
1169
- yy += 0.45;
1170
- }
1171
- if (it.desc) {
1172
- txt(slide, it.desc, { x: x + 0.2, y: yy, w: g.cw - 0.4, h: g.ch - (yy - y) - 0.2, fontSize: 11, color: ink2(), align: 'left', valign: 'top' });
1173
- }
1174
- });
1175
- footer(slide, ...foot(p));
1176
- }
1177
- function renderT08Bars(slide, p) {
1178
- bg(slide);
1179
- kicker(slide, p.kicker);
1180
- title(slide, p.title);
1181
- subtitle(slide, p.subtitle);
1182
- const items = (p.items || []).slice(0, 7);
1183
- const top = BODY_TOP + 0.1;
1184
- const rowH = (4.95 - top) / Math.max(items.length, 1);
1185
- const maxV = Math.max(1, ...items.map((d) => Number(d.value) || 0));
1186
- items.forEach((d, i) => {
1187
- const y = top + i * rowH;
1188
- txt(slide, d.label, { x: MX, y, w: 2.0, h: rowH * 0.7, fontSize: 13, color: ink(), align: 'left', valign: 'middle', fontFace: body() });
1189
- const trackX = MX + 2.1, trackW = CW - 2.1 - 1.0;
1190
- slide.addShape('roundRect', { x: trackX, y: y + rowH * 0.18, w: trackW, h: rowH * 0.5, rectRadius: 0.04, fill: { color: surfaceElev() } });
1191
- const fw = trackW * (Number(d.value) || 0) / maxV;
1192
- slide.addShape('roundRect', { x: trackX, y: y + rowH * 0.18, w: Math.max(0.05, fw), h: rowH * 0.5, rectRadius: 0.04, fill: { color: i % 2 ? gold() : 'C9962E' } });
1193
- txt(slide, String(d.value), { x: trackX + trackW + 0.05, y, w: 1.0, h: rowH * 0.7, fontSize: 14, color: ink(), bold: true, align: 'right', valign: 'middle', fontFace: heading() });
1194
- });
1195
- footer(slide, ...foot(p));
1196
- }
1197
- function renderT08Line(slide, p) {
1198
- bg(slide);
1199
- kicker(slide, p.kicker);
1200
- title(slide, p.title);
1201
- subtitle(slide, p.subtitle);
1202
- const items = (p.items || []).slice(0, 10);
1203
- const maxV = Math.max(1, ...items.map((d) => Number(d.value) || 0));
1204
- const baseY = 4.85;
1205
- const top = BODY_TOP + 0.2;
1206
- const avail = baseY - top;
1207
- const gap = 0.2;
1208
- const cw = (CW - gap * (items.length - 1)) / items.length;
1209
- items.forEach((d, i) => {
1210
- const x = MX + i * (cw + gap);
1211
- const h = avail * (Number(d.value) || 0) / maxV;
1212
- slide.addShape('rect', { x, y: baseY - h, w: cw, h, fill: { color: i % 2 ? gold() : 'C9962E' } });
1213
- txt(slide, String(d.value), { x, y: baseY - h - 0.3, w: cw, h: 0.3, fontSize: 11, color: ink(), align: 'center', bold: true, fontFace: heading() });
1214
- txt(slide, d.label, { x, y: baseY + 0.05, w: cw, h: 0.3, fontSize: 10, color: ink2(), align: 'center' });
1215
- });
1216
- footer(slide, ...foot(p));
1217
- }
1218
- function renderT08Dumbbell(slide, p) {
1219
- bg(slide);
1220
- kicker(slide, p.kicker);
1221
- title(slide, p.title);
1222
- subtitle(slide, p.subtitle);
1223
- const items = (p.items || []).slice(0, 7);
1224
- const top = BODY_TOP + 0.2;
1225
- const rowH = (4.85 - top) / Math.max(items.length, 1);
1226
- const maxV = Math.max(1, ...items.flatMap((d) => [Number(d.a) || 0, Number(d.b) || 0]));
1227
- items.forEach((d, i) => {
1228
- const y = top + i * rowH + rowH * 0.4;
1229
- txt(slide, d.label, { x: MX, y: y - 0.2, w: 1.8, h: 0.4, fontSize: 12, color: ink(), align: 'left', valign: 'middle' });
1230
- const trackX = MX + 1.9, trackW = CW - 1.9 - 0.9;
1231
- slide.addShape('roundRect', { x: trackX, y: y - 0.04, w: trackW, h: 0.08, rectRadius: 0.04, fill: { color: surfaceElev() } });
1232
- const ax = trackX + trackW * (Number(d.a) || 0) / maxV;
1233
- const bx = trackX + trackW * (Number(d.b) || 0) / maxV;
1234
- slide.addShape('oval', { x: ax - 0.12, y: y - 0.12, w: 0.24, h: 0.24, fill: { color: gold() } });
1235
- slide.addShape('oval', { x: bx - 0.12, y: y - 0.12, w: 0.24, h: 0.24, fill: { color: 'C9962E' } });
1236
- txt(slide, [d.a, d.b].filter(Boolean).join('→'), { x: trackX + trackW + 0.05, y: y - 0.2, w: 0.9, h: 0.4, fontSize: 11, color: ink2(), align: 'left' });
1237
- });
1238
- footer(slide, ...foot(p));
1239
- }
1240
- function renderT08Panels(slide, p) {
1241
- bg(slide);
1242
- kicker(slide, p.kicker);
1243
- title(slide, p.title);
1244
- subtitle(slide, p.subtitle);
1245
- const items = (p.items || []).slice(0, 5);
1246
- const top = BODY_TOP + 0.1;
1247
- const rowH = (4.9 - top) / Math.max(items.length, 1);
1248
- items.forEach((it, i) => {
1249
- const y = top + i * rowH;
1250
- card(slide, MX, y, CW, rowH - 0.15);
1251
- slide.addShape('rect', { x: MX, y, w: 0.08, h: rowH - 0.15, fill: { color: gold() } });
1252
- if (it.name)
1253
- txt(slide, it.name, { x: MX + 0.25, y: y + 0.12, w: 3.0, h: rowH - 0.3, fontSize: 16, color: ink(), bold: true, valign: 'middle', fontFace: heading() });
1254
- if (it.note)
1255
- txt(slide, it.note, { x: MX + 3.4, y: y + 0.12, w: CW - 3.6, h: rowH - 0.3, fontSize: 12, color: ink2(), valign: 'middle' });
1256
- });
1257
- footer(slide, ...foot(p));
1258
- }
1259
- function renderT08Statwall(slide, p, opts) {
1260
- bg(slide);
1261
- kicker(slide, p.kicker);
1262
- title(slide, p.title);
1263
- subtitle(slide, p.subtitle);
1264
- const items = (p.items || []).slice(0, 8);
1265
- const perRow = Math.min(opts.cols, items.length || 1);
1266
- const g = gridGeom(items.length || 1, perRow, BODY_TOP + 0.2);
1267
- items.forEach((it, i) => {
1268
- const r = Math.floor(i / perRow), c = i % perRow;
1269
- const x = MX + c * (g.cw + g.gap), y = g.top + r * (g.ch + g.gap);
1270
- card(slide, x, y, g.cw, g.ch);
1271
- txt(slide, [it.value, it.unit].filter(Boolean).join(' '), { x: x + 0.2, y: y + 0.2, w: g.cw - 0.4, h: 0.6, fontSize: 30, color: gold(), bold: true, fontFace: heading() });
1272
- txt(slide, it.label, { x: x + 0.2, y: y + 0.85, w: g.cw - 0.4, h: g.ch - 1.0, fontSize: 12, color: ink2(), valign: 'top' });
1273
- });
1274
- footer(slide, ...foot(p));
1275
- }
1276
- function renderT08ArchFlow(slide, p) {
1277
- bg(slide);
1278
- kicker(slide, p.kicker);
1279
- title(slide, p.title);
1280
- subtitle(slide, p.subtitle);
1281
- const nodes = (p.nodes || p.items || []).slice(0, 6);
1282
- const n = nodes.length || 1;
1283
- const gap = 0.25;
1284
- const cw = (CW - gap * (n - 1)) / n;
1285
- nodes.forEach((nd, i) => {
1286
- const x = MX + i * (cw + gap);
1287
- card(slide, x, BODY_TOP + 0.2, cw, 2.6);
1288
- if (nd.name)
1289
- txt(slide, nd.name, { x: x + 0.2, y: BODY_TOP + 0.45, w: cw - 0.4, h: 0.5, fontSize: 15, color: ink(), bold: true, fontFace: heading() });
1290
- if (nd.desc)
1291
- txt(slide, nd.desc, { x: x + 0.2, y: BODY_TOP + 1.0, w: cw - 0.4, h: 1.6, fontSize: 11, color: ink2(), valign: 'top' });
1292
- if (i < n - 1)
1293
- txt(slide, '→', { x: x + cw + 0.01, y: BODY_TOP + 1.3, w: gap, h: 0.5, fontSize: 18, color: gold(), align: 'center', bold: true });
1294
- });
1295
- footer(slide, ...foot(p));
1296
- }
1297
- function renderT08CapitalLoop(slide, p) {
1298
- bg(slide);
1299
- kicker(slide, p.kicker);
1300
- title(slide, p.title);
1301
- subtitle(slide, p.subtitle);
1302
- const steps = (p.steps || p.items || []).slice(0, 5);
1303
- const n = steps.length || 1;
1304
- const gap = 0.3;
1305
- const cw = (CW - gap * (n - 1)) / n;
1306
- steps.forEach((s, i) => {
1307
- const x = MX + i * (cw + gap);
1308
- card(slide, x, BODY_TOP + 0.4, cw, 2.2);
1309
- if (s.title)
1310
- txt(slide, s.title, { x: x + 0.15, y: BODY_TOP + 0.7, w: cw - 0.3, h: 0.5, fontSize: 14, color: ink(), bold: true, fontFace: heading() });
1311
- if (s.note)
1312
- txt(slide, s.note, { x: x + 0.15, y: BODY_TOP + 1.25, w: cw - 0.3, h: 1.2, fontSize: 11, color: ink2(), valign: 'top' });
1313
- if (i < n - 1)
1314
- txt(slide, '→', { x: x + cw + 0.01, y: BODY_TOP + 1.4, w: gap, h: 0.5, fontSize: 18, color: gold(), align: 'center', bold: true });
1315
- });
1316
- footer(slide, ...foot(p));
1317
- }
1318
- const RENDERERS = {
1319
- theme08_cover_v1: renderTheme08Cover,
1320
- theme08_chapter_v1: renderTheme08Chapter,
1321
- theme08_content_v1: renderTheme08Content,
1322
- theme08_contents_v1: renderTheme08Contents,
1323
- theme08_metrics_v1: renderTheme08Metrics,
1324
- theme08_metric_big_v1: renderTheme08MetricBig,
1325
- theme08_feature_v1: renderTheme08Feature,
1326
- theme08_overview_v1: renderTheme08Overview,
1327
- theme08_strategy_v1: renderTheme08Strategy,
1328
- theme08_process_v1: renderTheme08Process,
1329
- theme08_timeline_v1: renderTheme08Timeline,
1330
- theme08_roadmap_v1: renderTheme08Roadmap,
1331
- theme08_workflow_v1: renderTheme08Workflow,
1332
- theme08_team_v1: renderTheme08Team,
1333
- theme08_partners_v1: renderTheme08Partners,
1334
- theme08_gallery_v1: renderTheme08Gallery,
1335
- theme08_collage_v1: renderTheme08Collage,
1336
- theme08_bubble_v1: renderTheme08Bubble,
1337
- theme08_quote_v1: renderTheme08Quote,
1338
- theme08_closing_v1: renderTheme08Closing,
1339
- theme08_hero_split_v1: renderTheme08HeroSplit,
1340
- theme08_ecosystem_v1: renderTheme08Ecosystem,
1341
- theme08_case_v1: renderTheme08Case,
1342
- theme08_funding_v1: renderTheme08Funding,
1343
- theme08_matrix_v1: renderTheme08Matrix,
1344
- theme08_quadrant_v1: renderTheme08Quadrant,
1345
- theme08_scorecard_v1: renderTheme08Scorecard,
1346
- theme08_region_v1: renderTheme08Region,
1347
- theme08_range_v1: renderTheme08Range,
1348
- theme08_heatmap_v1: renderTheme08Heatmap,
1349
- theme08_waterfall_v1: renderTheme08Waterfall,
1350
- theme08_chart_bar_v1: renderTheme08ChartBar,
1351
- theme08_chart_donut_v1: renderTheme08ChartDonut,
1352
- theme08_gauge_v1: renderTheme08Gauge,
1353
- theme08_radar_v1: renderTheme08Radar,
1354
- theme08_ranking_v1: renderTheme08Ranking,
1355
- theme08_compare_v1: renderTheme08Compare,
1356
- theme08_table_v1: renderTheme08Table,
1357
- theme08_chain_v1: renderTheme08Chain,
1358
- // ---- v2 变体版式(对齐参考 theme08 共 84 组件页) ----
1359
- theme08_cover_v2: renderT08Cover2,
1360
- theme08_cover_v3: renderT08Cover2,
1361
- theme08_cover_v4: renderT08Cover2,
1362
- theme08_cover_v5: renderT08Cover2,
1363
- theme08_chapter_v2: renderT08Chapter2,
1364
- theme08_chapter_v3: renderT08Chapter2,
1365
- theme08_chapter_v4: renderT08Chapter2,
1366
- theme08_quote_statement_v1: renderT08Quote2,
1367
- theme08_quote_resources_v1: renderT08Quote2,
1368
- theme08_quote_verdict_v1: renderT08Quote2,
1369
- theme08_quote_twofield_v1: renderT08Quote2,
1370
- theme08_quote_manifesto_v1: renderT08Quote2,
1371
- theme08_case_card_v1: (s, p) => renderT08GridCards(s, p, { cols: 3 }),
1372
- theme08_case_card_v2: (s, p) => renderT08GridCards(s, p, { cols: 2 }),
1373
- theme08_case_table_v1: (s, p) => renderT08GridCards(s, p, { cols: 1, list: true }),
1374
- theme08_case_study_v1: (s, p) => renderT08GridCards(s, p, { cols: 2 }),
1375
- theme08_case_grid_v1: (s, p) => renderT08GridCards(s, p, { cols: 4 }),
1376
- theme08_case_list_v1: (s, p) => renderT08GridCards(s, p, { cols: 1, list: true }),
1377
- theme08_region_anchor_v1: (s, p) => renderT08Statwall(s, p, { cols: 1 }),
1378
- theme08_region_card_ny_v1: (s, p) => renderT08GridCards(s, p, { cols: 3 }),
1379
- theme08_region_dotmap_v1: (s, p) => renderT08GridCards(s, p, { cols: 4 }),
1380
- theme08_segment_v1: renderT08ArchFlow,
1381
- theme08_pipeline_v1: renderT08ArchFlow,
1382
- theme08_architecture_v1: renderT08ArchFlow,
1383
- theme08_supply_v1: renderT08ArchFlow,
1384
- theme08_compute_v1: renderT08ArchFlow,
1385
- theme08_trend_v1: renderT08Bars,
1386
- theme08_cross_v1: renderT08Bars,
1387
- theme08_peak_v1: renderT08Line,
1388
- theme08_pullback_v1: renderT08Dumbbell,
1389
- theme08_peak_trough_v1: renderT08Dumbbell,
1390
- theme08_capital_curve_v1: renderT08Line,
1391
- theme08_revenue_v1: renderT08Panels,
1392
- theme08_regulation_v1: renderT08Panels,
1393
- theme08_squeeze_v1: renderT08Panels,
1394
- theme08_early_stage_v1: (s, p) => renderT08Statwall(s, p, { cols: 4 }),
1395
- theme08_investor_mix_v1: (s, p) => renderT08GridCards(s, p, { cols: 3 }),
1396
- theme08_resource_map_v1: renderT08ArchFlow,
1397
- theme08_closed_loop_v1: renderT08CapitalLoop,
1398
- theme08_triptych_v1: (s, p) => renderT08GridCards(s, p, { cols: 3 }),
1399
- theme08_scene_split_v1: (s, p) => renderT08GridCards(s, p, { cols: 2 }),
1400
- theme08_budget_card_v1: (s, p) => renderT08GridCards(s, p, { cols: 3 }),
1401
- theme08_mainlines_v1: (s, p) => renderT08GridCards(s, p, { cols: 3 }),
1402
- theme08_migration_v1: renderT08CapitalLoop,
1403
- theme08_size_split_v1: renderT08Dumbbell,
1404
- };
1405
- /** 将全部 theme08 渲染器注册到导出管线的按 layoutId 索引注册表。 */
1406
- export function registerTheme08Renderers(register) {
1407
- for (const [id, fn] of Object.entries(RENDERERS)) {
1408
- register(id, fn);
1409
- }
1410
- }
1411
- //# sourceMappingURL=theme08-pptx.js.map