@lemonppt/renderer 1.0.6 → 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 (92) 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/editor-script.js +1 -2
  12. package/dist/client/theme-echarts.js +27 -27
  13. package/dist/editor-script.d.ts +1 -1
  14. package/dist/editor-script.d.ts.map +1 -1
  15. package/dist/editor-script.js +1 -2
  16. package/dist/editor-script.js.map +1 -1
  17. package/dist/export-pdf.d.ts.map +1 -1
  18. package/dist/export-pdf.js +7 -4
  19. package/dist/export-pdf.js.map +1 -1
  20. package/dist/export-pptx-screenshot.d.ts +36 -0
  21. package/dist/export-pptx-screenshot.d.ts.map +1 -0
  22. package/dist/export-pptx-screenshot.js +74 -0
  23. package/dist/export-pptx-screenshot.js.map +1 -0
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/normalize-goal.d.ts +6 -0
  29. package/dist/normalize-goal.d.ts.map +1 -0
  30. package/dist/normalize-goal.js +12 -0
  31. package/dist/normalize-goal.js.map +1 -0
  32. package/dist/preview-server.d.ts +9 -0
  33. package/dist/preview-server.d.ts.map +1 -0
  34. package/dist/preview-server.js +81 -0
  35. package/dist/preview-server.js.map +1 -0
  36. package/dist/render-editor-data.d.ts.map +1 -1
  37. package/dist/render-editor-data.js +29 -2
  38. package/dist/render-editor-data.js.map +1 -1
  39. package/dist/render.d.ts.map +1 -1
  40. package/dist/render.js +25 -5
  41. package/dist/render.js.map +1 -1
  42. package/package.json +8 -5
  43. package/templates/editor.html +26 -0
  44. package/templates/editor.js +1 -0
  45. package/dist/export-pptx.d.ts +0 -1167
  46. package/dist/export-pptx.d.ts.map +0 -1
  47. package/dist/export-pptx.js +0 -3007
  48. package/dist/export-pptx.js.map +0 -1
  49. package/dist/theme01-pptx.d.ts +0 -12
  50. package/dist/theme01-pptx.d.ts.map +0 -1
  51. package/dist/theme01-pptx.js +0 -2957
  52. package/dist/theme01-pptx.js.map +0 -1
  53. package/dist/theme02-pptx.d.ts +0 -5
  54. package/dist/theme02-pptx.d.ts.map +0 -1
  55. package/dist/theme02-pptx.js +0 -1063
  56. package/dist/theme02-pptx.js.map +0 -1
  57. package/dist/theme03-pptx.d.ts +0 -5
  58. package/dist/theme03-pptx.d.ts.map +0 -1
  59. package/dist/theme03-pptx.js +0 -3279
  60. package/dist/theme03-pptx.js.map +0 -1
  61. package/dist/theme04-pptx.d.ts +0 -6
  62. package/dist/theme04-pptx.d.ts.map +0 -1
  63. package/dist/theme04-pptx.js +0 -5496
  64. package/dist/theme04-pptx.js.map +0 -1
  65. package/dist/theme05-pptx.d.ts +0 -5
  66. package/dist/theme05-pptx.d.ts.map +0 -1
  67. package/dist/theme05-pptx.js +0 -3152
  68. package/dist/theme05-pptx.js.map +0 -1
  69. package/dist/theme06-pptx.d.ts +0 -124
  70. package/dist/theme06-pptx.d.ts.map +0 -1
  71. package/dist/theme06-pptx.js +0 -8154
  72. package/dist/theme06-pptx.js.map +0 -1
  73. package/dist/theme07-pptx.d.ts +0 -49
  74. package/dist/theme07-pptx.d.ts.map +0 -1
  75. package/dist/theme07-pptx.js +0 -2579
  76. package/dist/theme07-pptx.js.map +0 -1
  77. package/dist/theme08-pptx.d.ts +0 -19
  78. package/dist/theme08-pptx.d.ts.map +0 -1
  79. package/dist/theme08-pptx.js +0 -1411
  80. package/dist/theme08-pptx.js.map +0 -1
  81. package/dist/theme09-pptx.d.ts +0 -18
  82. package/dist/theme09-pptx.d.ts.map +0 -1
  83. package/dist/theme09-pptx.js +0 -1513
  84. package/dist/theme09-pptx.js.map +0 -1
  85. package/dist/theme10-pptx.d.ts +0 -18
  86. package/dist/theme10-pptx.d.ts.map +0 -1
  87. package/dist/theme10-pptx.js +0 -2889
  88. package/dist/theme10-pptx.js.map +0 -1
  89. package/dist/theme11-pptx.d.ts +0 -17
  90. package/dist/theme11-pptx.d.ts.map +0 -1
  91. package/dist/theme11-pptx.js +0 -908
  92. package/dist/theme11-pptx.js.map +0 -1
@@ -1,1513 +0,0 @@
1
- // lemonPPT - AI-powered presentation generation
2
- // Copyright (c) 2026 lemonforme
3
- // SPDX-License-Identifier: AGPL-3.0-or-later
4
- //
5
- // theme09 墨韵专色 · 杂志印刷风 — PPTX 版式专属渲染器。
6
- // 解决 theme09 在 PPTX 导出中落到 "Unknown layout" 占位的问题。
7
- //
8
- // 设计要点:
9
- // - 双基底(substrate)—— 每个版式**预分配**基底,翻页形成杂志跨页呼吸节奏:
10
- // paper 纸底 #F4F1EA(数据/目录/正文密集页)
11
- // ink 墨底 #14161C(影像/章节/金句/跨页)
12
- // - appearance(primary / muted)不翻转明暗,只调专色浓度与网点密度。
13
- // - 所有色值取自 tokens.ts 的预校验专色系统,未复制 third-party theme09 实现。
14
- let S = {
15
- appearance: 'primary',
16
- fonts: { heading: 'Noto Serif SC', body: 'Noto Sans SC', mono: 'Space Mono' },
17
- chartColors: ['C8102E', '2B4A8B', '8F6410', '3F6B5C', '6D4C7D', 'A8562A'],
18
- };
19
- /** 每次导出开始时由 export-pptx.ts 注入 appearance / 字体 / 图表色。 */
20
- export function configureTheme09(state) {
21
- S = state;
22
- }
23
- // ────────────────────────────────────────────────────────────────
24
- // 双基底专色系统(与 tokens.ts 同源,PPTX 不支持 color-mix 故固化)
25
- // ────────────────────────────────────────────────────────────────
26
- const PAPER = {
27
- bg: 'F4F1EA',
28
- ink: '14161C',
29
- ink2: '3A3F4A',
30
- ink3: '5C6270',
31
- surface: 'FDFCF8',
32
- surfaceElevated: 'FFFFFF',
33
- border: 'D5CFC0',
34
- borderStrong: 'B7AE99',
35
- };
36
- const INK = {
37
- bg: '14161C',
38
- ink: 'F4F1EA',
39
- ink2: 'C9C6BE',
40
- ink3: '8E8B84',
41
- surface: '1C2027',
42
- surfaceElevated: '242932',
43
- border: '2E323C',
44
- borderStrong: '4A4F5B',
45
- };
46
- const SPOT = {
47
- paper: {
48
- primary: { accent: 'C8102E', accent2: '2B4A8B', spot3: '8F6410', spot4: '3F6B5C', spot5: '6D4C7D', spot6: 'A8562A' },
49
- muted: { accent: '9C2233', accent2: '3C5580', spot3: '7A5518', spot4: '3A5F53', spot5: '61496D', spot6: '8F4E2C' },
50
- },
51
- ink: {
52
- primary: { accent: 'F04A62', accent2: '5B82D6', spot3: 'D9A83C', spot4: '6FA894', spot5: 'A98BC4', spot6: 'E08A5B' },
53
- muted: { accent: 'D07080', accent2: '7E9BC8', spot3: 'C0A068', spot4: '8AAFA0', spot5: 'B0A0C4', spot6: 'CE9878' },
54
- },
55
- };
56
- /** 版式 → 预分配基底(与 Sheet substrate 完全一致)。 */
57
- const SUBSTRATE = {
58
- theme09_cover_masthead_v1: 'ink',
59
- theme09_cover_bleed_v1: 'ink',
60
- theme09_cover_dossier_v1: 'paper',
61
- theme09_cover_colorbar_v1: 'paper',
62
- theme09_cover_aperture_v1: 'ink',
63
- theme09_cover_colophon_v1: 'paper',
64
- theme09_cover_photo_v1: 'ink',
65
- theme09_abstract_v1: 'paper',
66
- theme09_contents_v1: 'paper',
67
- theme09_section_v1: 'ink',
68
- theme09_section_card_v1: 'paper',
69
- theme09_closing_v1: 'ink',
70
- 'theme09_cross-perspective_v1': 'ink',
71
- theme09_thesis_v1: 'paper',
72
- 'theme09_value-chain_v1': 'paper',
73
- theme09_risk_v1: 'ink',
74
- theme09_outlook_v1: 'ink',
75
- theme09_conclusion_v1: 'paper',
76
- theme09_bracket_v1: 'paper',
77
- theme09_flow_v1: 'ink',
78
- theme09_orbit_v1: 'ink',
79
- theme09_vertical_v1: 'paper',
80
- theme09_calendar_v1: 'paper',
81
- theme09_phases_v1: 'ink',
82
- theme09_gauge_v1: 'ink',
83
- theme09_scoreboard_v1: 'ink',
84
- theme09_trend_v1: 'paper',
85
- theme09_histogram_v1: 'paper',
86
- 'theme09_forecast-fan_v1': 'ink',
87
- theme09_plans_v1: 'paper',
88
- theme09_stair_v1: 'ink',
89
- theme09_stacked_v1: 'paper',
90
- theme09_era_v1: 'ink',
91
- theme09_roadmap_v1: 'paper',
92
- theme09_score_v1: 'ink',
93
- theme09_takeaway_v1: 'ink',
94
- theme09_compare_v1: 'paper',
95
- theme09_process_v1: 'paper',
96
- theme09_faq_v1: 'paper',
97
- // ── P2 图表型(数据/目录/密集页,纸底)──
98
- theme09_market_overview_v1: 'paper',
99
- 'theme09_streamgraph_v1': 'paper',
100
- 'theme09_chord_v1': 'paper',
101
- 'theme09_sunburst_v1': 'paper',
102
- 'theme09_ribbon_v1': 'paper',
103
- 'theme09_rounds_v1': 'paper',
104
- 'theme09_ranking_v1': 'paper',
105
- 'theme09_bump_v1': 'paper',
106
- 'theme09_hero_number_v1': 'paper',
107
- 'theme09_versus_v1': 'paper',
108
- 'theme09_spiral_v1': 'paper',
109
- 'theme09_funnel_v1': 'paper',
110
- 'theme09_stat_grid_v1': 'paper',
111
- 'theme09_arc_v1': 'paper',
112
- 'theme09_network_v1': 'paper',
113
- 'theme09_area_v1': 'paper',
114
- 'theme09_mega_number_v1': 'paper',
115
- 'theme09_radar_v1': 'paper',
116
- 'theme09_radialbar_v1': 'paper',
117
- 'theme09_honeycomb_v1': 'paper',
118
- 'theme09_tornado_v1': 'paper',
119
- 'theme09_matrix_v1': 'paper',
120
- 'theme09_quadrant_v1': 'paper',
121
- 'theme09_bubble_v1': 'paper',
122
- 'theme09_marimekko_v1': 'paper',
123
- 'theme09_meter_v1': 'paper',
124
- 'theme09_parallel_v1': 'paper',
125
- 'theme09_grade_v1': 'paper',
126
- 'theme09_slope_v1': 'paper',
127
- 'theme09_dumbbell_v1': 'paper',
128
- 'theme09_crosstab_v1': 'paper',
129
- 'theme09_tier_v1': 'paper',
130
- 'theme09_ledger_v1': 'paper',
131
- 'theme09_alloc_v1': 'paper',
132
- 'theme09_venn_v1': 'paper',
133
- 'theme09_treemap_v1': 'paper',
134
- 'theme09_icicle_v1': 'paper',
135
- 'theme09_waterfall_v1': 'paper',
136
- 'theme09_heatmap_v1': 'paper',
137
- // ── P1 图片 / 标本 / 金句型(影像/跨页,墨底)──
138
- 'theme09_specimen_v1': 'ink',
139
- 'theme09_photo_feature_v1': 'ink',
140
- 'theme09_photo_grid_v1': 'ink',
141
- 'theme09_photo_quote_v1': 'ink',
142
- 'theme09_photo_duo_v1': 'ink',
143
- 'theme09_photo_panorama_v1': 'ink',
144
- 'theme09_photo_stage_v1': 'ink',
145
- 'theme09_storyboard_v1': 'ink',
146
- 'theme09_snapshot_tape_v1': 'ink',
147
- 'theme09_epigraph_v1': 'ink',
148
- 'theme09_photo_bento_v1': 'ink',
149
- 'theme09_timeline_photo_v1': 'ink',
150
- 'theme09_filmstrip_v1': 'ink',
151
- 'theme09_mosaic_v1': 'ink',
152
- 'theme09_testimonial_v1': 'ink',
153
- 'theme09_team_v1': 'ink',
154
- 'theme09_photo_ring_v1': 'ink',
155
- 'theme09_divider_photo_v1': 'ink',
156
- 'theme09_coverstory_v1': 'ink',
157
- 'theme09_quote_portrait_v1': 'ink',
158
- 'theme09_manifesto_v1': 'ink',
159
- 'theme09_annotated_v1': 'ink',
160
- 'theme09_diptych_v1': 'ink',
161
- 'theme09_case_folio_v1': 'ink',
162
- 'theme09_split_diagonal_v1': 'ink',
163
- 'theme09_typeriver_v1': 'ink',
164
- 'theme09_exhibit_wall_v1': 'ink',
165
- 'theme09_masonry_v1': 'ink',
166
- 'theme09_journey_v1': 'ink',
167
- 'theme09_photo_cards_v1': 'ink',
168
- 'theme09_zine_spread_v1': 'ink',
169
- 'theme09_photo_scene_v1': 'ink',
170
- 'theme09_spotlight_v1': 'ink',
171
- 'theme09_profile_v1': 'ink',
172
- 'theme09_gallery_wall_v1': 'ink',
173
- 'theme09_dotmatrix_v1': 'ink',
174
- };
175
- function P(layoutId) {
176
- const sub = SUBSTRATE[layoutId] ?? 'paper';
177
- const base = sub === 'ink' ? INK : PAPER;
178
- const spot = SPOT[sub][S.appearance];
179
- return { sub, isInk: sub === 'ink', ...base, ...spot };
180
- }
181
- function series(pal) {
182
- return [pal.accent, pal.accent2, pal.spot3, pal.spot4, pal.spot5, pal.spot6];
183
- }
184
- // ────────────────────────────────────────────────────────────────
185
- // 画布常量(16:9,10 × 5.625 英寸)
186
- // ────────────────────────────────────────────────────────────────
187
- const MX = 0.7;
188
- const CW = 10 - MX * 2;
189
- const TOP = 0.62;
190
- const BOT = 5.18;
191
- function lines(v) {
192
- if (v == null)
193
- return [];
194
- if (Array.isArray(v))
195
- return v.map(String);
196
- return String(v).split(/\r?\n/).filter((s) => s.length > 0);
197
- }
198
- function txt(slide, text, o) {
199
- if (text == null || text === '')
200
- return;
201
- if (Array.isArray(text)) {
202
- if (text.length === 0)
203
- return;
204
- text = text.map(String).join('\n');
205
- }
206
- slide.addText(text, o);
207
- }
208
- function frame(slide, pal, opts = {}) {
209
- slide.background = { color: pal.bg };
210
- slide.addShape('line', { x: MX, y: TOP, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
211
- if (opts.bottom !== false) {
212
- slide.addShape('line', { x: MX, y: BOT, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
213
- }
214
- }
215
- function kicker(slide, text, pal, o = {}) {
216
- txt(slide, text, {
217
- x: o.x ?? MX, y: o.y ?? 0.74, w: o.w ?? CW, h: 0.3,
218
- fontSize: o.size ?? 12, color: pal.accent, align: 'left',
219
- fontFace: S.fonts.mono, charSpacing: 2, bold: true,
220
- });
221
- }
222
- function title(slide, text, pal, o = {}) {
223
- const ls = lines(text);
224
- const size = o.size ?? 40;
225
- const h = o.h ?? ls.length * (size / 72) * 1.18 + 0.15;
226
- slide.addText(ls.join('\n'), {
227
- x: o.x ?? MX, y: o.y ?? 1.0, w: o.w ?? CW, h,
228
- fontSize: size, color: o.color ?? pal.ink, align: o.align ?? 'left',
229
- fontFace: S.fonts.heading, bold: true, valign: o.valign ?? 'top',
230
- lineSpacingMultiple: 1.05, rotate: o.rotate,
231
- });
232
- }
233
- function footer(slide, left, right, pal) {
234
- txt(slide, left, { x: MX, y: BOT + 0.04, w: CW / 2, h: 0.3, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
235
- txt(slide, right, { x: MX + CW / 2, y: BOT + 0.04, w: CW / 2, h: 0.3, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
236
- }
237
- function card(slide, x, y, w, h, pal, fill, stroke) {
238
- slide.addShape('rect', { x, y, w, h, fill: { color: fill ?? pal.surfaceElevated }, line: { color: stroke ?? pal.borderStrong, width: 1 } });
239
- }
240
- function colorBar(slide, x, y, w, pal) {
241
- const n = 6;
242
- const g = 0.08;
243
- const cw = (w - g * (n - 1)) / n;
244
- const s = series(pal);
245
- for (let i = 0; i < n; i++) {
246
- slide.addShape('rect', { x: x + i * (cw + g), y, w: cw, h: 0.16, fill: { color: s[i] } });
247
- }
248
- }
249
- function masthead(slide, p, pal) {
250
- txt(slide, p.section, { x: MX, y: 0.74, w: 4, h: 0.3, fontSize: 13, color: pal.ink, bold: true, align: 'left', fontFace: S.fonts.heading });
251
- txt(slide, p.sectionEn, { x: MX, y: 1.06, w: 4, h: 0.25, fontSize: 10, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
252
- txt(slide, p.mark, { x: MX + CW - 3, y: 0.74, w: 3, h: 0.3, fontSize: 11, color: pal.accent, align: 'right', bold: true, fontFace: S.fonts.mono });
253
- slide.addShape('line', { x: MX, y: 1.42, w: CW, h: 0, line: { color: pal.ink, width: 1.25 } });
254
- }
255
- function folio(slide, p, pal) {
256
- footer(slide, p.folioLeft, [p.folioPage, p.folioRight].filter(Boolean).join(' / '), pal);
257
- }
258
- function apertureFallback(slide, x, y, d, pal) {
259
- slide.addShape('ellipse', { x, y, w: d, h: d, fill: { color: pal.surfaceElevated }, line: { color: pal.border, width: 1 } });
260
- txt(slide, 'IMG', { x, y, w: d, h: d, fontSize: 16, color: pal.ink3, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono });
261
- }
262
- function toImgUrl(v) {
263
- if (!v)
264
- return undefined;
265
- if (typeof v === 'string')
266
- return v;
267
- if (typeof v === 'object') {
268
- const o = v;
269
- for (const k of ['url', 'src', 'href', 'path', 'image']) {
270
- const val = o[k];
271
- if (typeof val === 'string' && val.length)
272
- return val;
273
- }
274
- }
275
- return undefined;
276
- }
277
- function tryImage(slide, url, x, y, w, h, fallback) {
278
- const u = toImgUrl(url);
279
- // 仅接受可直读的本地路径或远程 http(s) 资源;data: URI 等内联占位图在 pptxgenjs 写盘时
280
- // 会触发 ENAMETOOLONG/无法读取,故直接降级为 fallback 卡片。
281
- if (!u || (!u.startsWith('http://') && !u.startsWith('https://') && !u.startsWith('/') && !u.startsWith('.'))) {
282
- fallback();
283
- return;
284
- }
285
- try {
286
- slide.addImage({ path: u, x, y, w, h });
287
- }
288
- catch {
289
- fallback();
290
- }
291
- }
292
- // =====================================================================
293
- // 1. cover · 刊头封面(ink)
294
- // =====================================================================
295
- function renderCoverMasthead(slide, p) {
296
- const pal = P('theme09_cover_masthead_v1');
297
- slide.background = { color: pal.bg };
298
- slide.addShape('rect', { x: 0, y: 0, w: 10, h: 0.34, fill: { color: pal.accent } });
299
- txt(slide, [p.issueEn, p.issue].filter(Boolean).join(' · '), {
300
- x: MX, y: 0, w: CW, h: 0.34, fontSize: 11, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2,
301
- });
302
- title(slide, p.title, pal, { x: MX, y: 0.72, w: CW, size: 46, align: 'center' });
303
- txt(slide, [p.strapline, p.straplineEn].filter(Boolean).join(' · '), {
304
- x: MX, y: 1.55, w: CW, h: 0.32, fontSize: 13, color: pal.accent, align: 'center', fontFace: S.fonts.mono, charSpacing: 1, bold: true,
305
- });
306
- title(slide, p.kick, pal, { x: MX, y: 2.05, w: CW, size: 30, align: 'center' });
307
- txt(slide, p.lead, { x: MX, y: 3.05, w: CW * 0.6, h: 1.5, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.25 });
308
- const items = (p.items || []).slice(0, 5);
309
- let iy = 3.05;
310
- items.forEach((t, i) => {
311
- txt(slide, String(i + 1).padStart(2, '0'), { x: MX + CW * 0.62, y: iy, w: 0.5, h: 0.3, fontSize: 13, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.mono });
312
- txt(slide, t, { x: MX + CW * 0.62 + 0.5, y: iy, w: CW * 0.38 - 0.5, h: 0.3, fontSize: 12, color: pal.ink, align: 'left', fontFace: S.fonts.body });
313
- iy += 0.34;
314
- });
315
- colorBar(slide, MX, 4.75, 3.2, pal);
316
- txt(slide, p.sign, { x: MX + 3.5, y: 4.7, w: CW - 3.5, h: 0.32, fontSize: 10, color: pal.ink3, align: 'right', fontFace: S.fonts.mono, charSpacing: 1 });
317
- }
318
- // =====================================================================
319
- // 2. cover · 出血斜切封面(ink)
320
- // =====================================================================
321
- function renderCoverBleed(slide, p) {
322
- const pal = P('theme09_cover_bleed_v1');
323
- slide.background = { color: pal.bg };
324
- slide.addShape('rect', { x: 0, y: 0, w: 3.2, h: 5.625, fill: { color: pal.accent } });
325
- txt(slide, p.railTop, { x: 0.35, y: 0.5, w: 2.5, h: 0.3, fontSize: 12, color: pal.bg, align: 'left', fontFace: S.fonts.mono, bold: true, charSpacing: 2 });
326
- txt(slide, p.vertical, { x: 0.7, y: 0.7, w: 2.0, h: 4.0, fontSize: 26, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading, rotate: 270 });
327
- txt(slide, p.railFoot, { x: 0.35, y: 5.05, w: 2.5, h: 0.3, fontSize: 11, color: pal.bg, align: 'left', fontFace: S.fonts.mono, charSpacing: 2 });
328
- const rx = 3.7;
329
- title(slide, p.title, pal, { x: rx, y: 1.3, w: 5.8, size: 44, color: pal.ink });
330
- txt(slide, p.subtitle, { x: rx, y: 3.0, w: 5.8, h: 1.0, fontSize: 15, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
331
- const meta = (p.metaItems || []).slice(0, 4);
332
- let my = 4.2;
333
- meta.forEach((m) => {
334
- txt(slide, m.k, { x: rx, y: my, w: 1.6, h: 0.3, fontSize: 10, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.mono });
335
- txt(slide, m.v, { x: rx + 1.6, y: my, w: 4.0, h: 0.3, fontSize: 12, color: pal.ink, align: 'left', fontFace: S.fonts.body });
336
- my += 0.38;
337
- });
338
- slide.addShape('line', { x: rx, y: 5.18, w: 10 - rx - MX, h: 0, line: { color: pal.border, width: 0.75 } });
339
- }
340
- // =====================================================================
341
- // 3. cover · 卷宗封面(paper)
342
- // =====================================================================
343
- function renderCoverDossier(slide, p) {
344
- const pal = P('theme09_cover_dossier_v1');
345
- frame(slide, pal);
346
- if (p.classif) {
347
- slide.addShape('rect', { x: MX, y: 0.85, w: 1.9, h: 0.6, fill: { color: pal.bg }, line: { color: pal.accent, width: 1.5 } });
348
- txt(slide, p.classif, { x: MX, y: 0.85, w: 1.9, h: 0.6, fontSize: 14, color: pal.accent, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2 });
349
- if (p.stampNote)
350
- txt(slide, p.stampNote, { x: MX + 2.1, y: 0.95, w: 3, h: 0.45, fontSize: 11, color: pal.ink2, align: 'left', valign: 'middle', fontFace: S.fonts.body });
351
- }
352
- title(slide, p.title, pal, { x: MX, y: 1.7, w: 5.6, size: 40 });
353
- txt(slide, p.subtitle, { x: MX, y: 2.6, w: 5.6, h: 1.1, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
354
- const fields = (p.fields || []).slice(0, 6);
355
- const colW = (CW - 0.4) / 2;
356
- fields.forEach((f, i) => {
357
- const cx = MX + (i % 2) * (colW + 0.4);
358
- const cy = 3.9 + Math.floor(i / 2) * 0.36;
359
- txt(slide, f.k, { x: cx, y: cy, w: 1.4, h: 0.3, fontSize: 10, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
360
- txt(slide, f.v, { x: cx + 1.4, y: cy, w: colW - 1.4, h: 0.3, fontSize: 12, color: pal.ink, align: 'left', bold: true, fontFace: S.fonts.body });
361
- });
362
- const sx = MX + 6.4;
363
- if (p.memo)
364
- txt(slide, p.memo, { x: sx, y: 1.7, w: CW - sx + MX, h: 1.4, fontSize: 14, color: pal.ink, align: 'left', fontFace: 'Caveat', valign: 'top' });
365
- if (p.chop) {
366
- slide.addShape('ellipse', { x: sx + 0.2, y: 3.2, w: 1.1, h: 1.1, fill: { color: pal.bg }, line: { color: pal.accent, width: 2 } });
367
- txt(slide, p.chop, { x: sx + 0.2, y: 3.2, w: 1.1, h: 1.1, fontSize: 16, color: pal.accent, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading });
368
- }
369
- const meta = (p.metaLines || []).slice(0, 4);
370
- let my = 4.5;
371
- meta.forEach((t) => {
372
- txt(slide, t, { x: sx, y: my, w: CW - sx + MX, h: 0.28, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
373
- my += 0.3;
374
- });
375
- }
376
- // =====================================================================
377
- // 4. cover · 色标封面(paper)
378
- // =====================================================================
379
- function renderCoverColorbar(slide, p) {
380
- const pal = P('theme09_cover_colorbar_v1');
381
- frame(slide, pal);
382
- const s1 = series(pal);
383
- const cells = [...s1, pal.ink, pal.ink3];
384
- const codes = ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'K100', 'K40'];
385
- const n = 8;
386
- const g = 0.1;
387
- const cw = (CW - g * (n - 1)) / n;
388
- const gy = 0.85;
389
- const ch = 0.9;
390
- cells.forEach((c, i) => {
391
- const x = MX + i * (cw + g);
392
- slide.addShape('rect', { x, y: gy, w: cw, h: ch, fill: { color: c }, line: { color: pal.border, width: 0.5 } });
393
- txt(slide, codes[i], { x, y: gy + ch + 0.02, w: cw, h: 0.22, fontSize: 8, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
394
- });
395
- title(slide, p.title, pal, { x: MX, y: 2.1, w: 5.6, size: 44 });
396
- txt(slide, p.subtitle, { x: MX, y: 3.1, w: 5.6, h: 1.2, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
397
- const specs = (p.specs || []).slice(0, 6);
398
- const sx = MX + 5.9;
399
- let sy = 2.1;
400
- specs.forEach((sp) => {
401
- txt(slide, sp.k, { x: sx, y: sy, w: 1.6, h: 0.3, fontSize: 10, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
402
- txt(slide, sp.v, { x: sx + 1.6, y: sy, w: CW - sx - MX - 1.6, h: 0.3, fontSize: 13, color: pal.ink, align: 'left', bold: true, fontFace: S.fonts.body });
403
- sy += 0.4;
404
- });
405
- footer(slide, p.footLeft, p.footRight, pal);
406
- }
407
- // =====================================================================
408
- // 5. cover · 圆窗封面(ink)
409
- // =====================================================================
410
- function renderCoverAperture(slide, p) {
411
- const pal = P('theme09_cover_aperture_v1');
412
- frame(slide, pal, { bottom: false });
413
- if (p.tag)
414
- kicker(slide, p.tag, pal, { x: MX, y: 0.85 });
415
- title(slide, p.title, pal, { x: MX, y: 1.25, w: 5.4, size: 42 });
416
- txt(slide, p.subtitle, { x: MX, y: 3.3, w: 5.4, h: 1.2, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
417
- const tags = (p.tags || []).slice(0, 5);
418
- let ty = 4.6;
419
- tags.forEach((t) => {
420
- const w = 0.3 + t.length * 0.16;
421
- slide.addShape('roundRect', { x: MX, y: ty, w, h: 0.34, rectRadius: 0.17, fill: { color: pal.bg }, line: { color: pal.accent, width: 1 } });
422
- txt(slide, t, { x: MX, y: ty, w, h: 0.34, fontSize: 11, color: pal.accent, align: 'center', valign: 'middle', fontFace: S.fonts.mono });
423
- ty += 0.42;
424
- });
425
- const cx = 6.7;
426
- const cy = 1.0;
427
- const d = 3.0;
428
- tryImage(slide, p.imageUrl, cx, cy, d, d, () => apertureFallback(slide, cx, cy, d, pal));
429
- slide.addShape('ellipse', { x: cx - 0.06, y: cy - 0.06, w: d + 0.12, h: d + 0.12, fill: { color: 'FFFFFF', transparency: 100 }, line: { color: pal.accent, width: 2 } });
430
- footer(slide, p.tag, 'APERTURE', pal);
431
- }
432
- // =====================================================================
433
- // 6. cover · 版本页封面(paper)
434
- // =====================================================================
435
- function renderCoverColophon(slide, p) {
436
- const pal = P('theme09_cover_colophon_v1');
437
- frame(slide, pal);
438
- title(slide, p.title, pal, { x: MX, y: 0.85, w: CW, size: 40 });
439
- txt(slide, p.titleEn, { x: MX, y: 1.65, w: CW, h: 0.3, fontSize: 13, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
440
- const cols = (p.cols || []).slice(0, 4);
441
- const gap = 0.3;
442
- const cw = (CW - gap * (cols.length - 1)) / cols.length;
443
- cols.forEach((c, i) => {
444
- const x = MX + i * (cw + gap);
445
- txt(slide, c.heading, { x, y: 2.2, w: cw, h: 0.3, fontSize: 13, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.heading });
446
- slide.addShape('line', { x, y: 2.56, w: cw, h: 0, line: { color: pal.ink, width: 1 } });
447
- (c.lines || []).slice(0, 6).forEach((ln, j) => {
448
- txt(slide, ln, { x, y: 2.66 + j * 0.34, w: cw, h: 0.32, fontSize: 11, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
449
- });
450
- });
451
- colorBar(slide, MX, 4.9, 3.2, pal);
452
- txt(slide, p.footLeft, { x: MX + 3.5, y: 4.86, w: CW - 3.5, h: 0.3, fontSize: 10, color: pal.ink3, align: 'right', fontFace: S.fonts.mono, charSpacing: 1 });
453
- }
454
- // =====================================================================
455
- // 7. cover · 影像封面(ink)
456
- // =====================================================================
457
- function renderCoverPhoto(slide, p) {
458
- const pal = P('theme09_cover_photo_v1');
459
- slide.background = { color: pal.bg };
460
- tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => { });
461
- slide.addShape('rect', { x: 0, y: 3.4, w: 10, h: 2.225, fill: { color: '000000', transparency: 35 } });
462
- if (p.strip) {
463
- const w = 0.3 + p.strip.length * 0.12;
464
- slide.addShape('rect', { x: MX, y: 0.9, w, h: 0.36, fill: { color: pal.accent } });
465
- txt(slide, p.strip, { x: MX, y: 0.9, w, h: 0.36, fontSize: 12, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2 });
466
- }
467
- title(slide, p.title, pal, { x: MX, y: 3.7, w: CW, size: 44, color: pal.ink });
468
- txt(slide, p.subtitle, { x: MX, y: 4.6, w: CW, h: 0.7, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
469
- const meta = (p.metaItems || []).slice(0, 4);
470
- txt(slide, meta.join(' / '), { x: MX, y: 5.25, w: CW, h: 0.3, fontSize: 11, color: pal.ink2, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
471
- }
472
- // =====================================================================
473
- // 8. content · 卷首摘要(paper)
474
- // =====================================================================
475
- function renderAbstract(slide, p) {
476
- const pal = P('theme09_abstract_v1');
477
- frame(slide, pal, { bottom: false });
478
- masthead(slide, p, pal);
479
- title(slide, p.title, pal, { x: MX, y: 1.7, w: 5.6, size: 34 });
480
- txt(slide, p.standfirst, { x: MX, y: 2.3, w: 5.6, h: 2.6, fontSize: 14, color: pal.ink, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.25 });
481
- const figs = (p.figures || []).slice(0, 5);
482
- const sx = MX + 6.0;
483
- slide.addShape('line', { x: sx - 0.3, y: 1.9, w: 0, h: 3.0, line: { color: pal.accent, width: 2 } });
484
- let fy = 1.9;
485
- figs.forEach((f) => {
486
- txt(slide, [f.v, f.unit].filter(Boolean).join(''), { x: sx, y: fy, w: CW - sx + MX, h: 0.6, fontSize: 30, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.heading });
487
- txt(slide, f.k, { x: sx, y: fy + 0.6, w: CW - sx + MX, h: 0.3, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
488
- fy += 0.95;
489
- });
490
- folio(slide, p, pal);
491
- }
492
- // =====================================================================
493
- // 9. tableOfContents · 目录(paper)
494
- // =====================================================================
495
- function renderContents(slide, p) {
496
- const pal = P('theme09_contents_v1');
497
- frame(slide, pal, { bottom: false });
498
- masthead(slide, p, pal);
499
- title(slide, p.title, pal, { x: MX, y: 1.7, w: 6, size: 34 });
500
- txt(slide, p.titleEn, { x: MX + 6, y: 1.85, w: 3, h: 0.3, fontSize: 13, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
501
- const items = (p.items || []).slice(0, 10);
502
- let y = 2.5;
503
- const rowH = (5.0 - 2.5) / Math.max(items.length, 1);
504
- items.forEach((it, i) => {
505
- const col = it.current ? pal.accent : pal.ink;
506
- txt(slide, String(i + 1).padStart(2, '0'), { x: MX, y, w: 0.6, h: rowH, fontSize: 14, color: col, align: 'left', valign: 'middle', bold: true, fontFace: S.fonts.mono });
507
- txt(slide, it.name, { x: MX + 0.7, y, w: 4.0, h: rowH, fontSize: 15, color: col, align: 'left', valign: 'middle', bold: it.current, fontFace: S.fonts.heading });
508
- txt(slide, it.desc || '', { x: MX + 4.8, y, w: 3.0, h: rowH, fontSize: 11, color: pal.ink3, align: 'left', valign: 'middle', fontFace: S.fonts.body });
509
- txt(slide, it.page || '', { x: MX + CW - 1.0, y, w: 1.0, h: rowH, fontSize: 14, color: col, align: 'right', valign: 'middle', bold: true, fontFace: S.fonts.mono });
510
- if (i < items.length - 1)
511
- slide.addShape('line', { x: MX, y: y + rowH, w: CW, h: 0, line: { color: pal.border, width: 0.5 } });
512
- y += rowH;
513
- });
514
- folio(slide, p, pal);
515
- }
516
- // =====================================================================
517
- // 10. content · 篇章扉页(ink)
518
- // =====================================================================
519
- function renderSection(slide, p) {
520
- const pal = P('theme09_section_v1');
521
- frame(slide, pal, { bottom: false });
522
- title(slide, p.num, pal, { x: 0.2, y: 3.0, w: 4.5, h: 2.4, size: 180, color: pal.accent, align: 'left', valign: 'bottom' });
523
- txt(slide, p.name, { x: 8.6, y: 0.7, w: 1.2, h: 4.0, fontSize: 40, color: pal.ink, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading, rotate: 270 });
524
- if (p.nameEn)
525
- kicker(slide, p.nameEn, pal, { x: 4.6, y: 1.0 });
526
- txt(slide, p.lede, { x: 4.6, y: 1.5, w: 3.8, h: 1.8, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
527
- const pts = (p.points || []).slice(0, 4);
528
- let py = 3.5;
529
- pts.forEach((t) => {
530
- txt(slide, '— ' + t, { x: 4.6, y: py, w: 3.9, h: 0.35, fontSize: 12, color: pal.ink, align: 'left', fontFace: S.fonts.body });
531
- py += 0.4;
532
- });
533
- folio(slide, p, pal);
534
- }
535
- // =====================================================================
536
- // 11. content · 篇章卡(paper)
537
- // =====================================================================
538
- function renderSectionCard(slide, p) {
539
- const pal = P('theme09_section_card_v1');
540
- frame(slide, pal, { bottom: false });
541
- masthead(slide, p, pal);
542
- const cardX = MX;
543
- const cardY = 1.7;
544
- const cardW = 3.6;
545
- const cardH = 3.0;
546
- card(slide, cardX, cardY, cardW, cardH, pal, pal.surfaceElevated, pal.borderStrong);
547
- slide.addShape('rect', { x: cardX, y: cardY, w: cardW, h: 0.12, fill: { color: pal.accent } });
548
- slide.addShape('triangle', { x: cardX + cardW - 0.5, y: cardY, w: 0.5, h: 0.5, fill: { color: pal.bg }, line: { color: pal.border, width: 1 }, rotate: 180, flipH: true });
549
- title(slide, p.num, pal, { x: cardX + 0.3, y: cardY + 0.3, w: cardW - 0.6, h: 1.0, size: 60, color: pal.accent });
550
- title(slide, p.name, pal, { x: cardX + 0.3, y: cardY + 1.4, w: cardW - 0.6, h: 0.9, size: 28 });
551
- if (p.nameEn)
552
- txt(slide, p.nameEn, { x: cardX + 0.3, y: cardY + 2.3, w: cardW - 0.6, h: 0.3, fontSize: 12, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
553
- const pts = (p.points || []).slice(0, 4);
554
- const sx = MX + 4.0;
555
- let py = 1.8;
556
- pts.forEach((pt, i) => {
557
- slide.addShape('line', { x: sx, y: py, w: CW - sx + MX, h: 0, line: { color: pal.borderStrong, width: 1.25 } });
558
- txt(slide, String(i + 1).padStart(2, '0'), { x: sx, y: py + 0.1, w: 0.6, h: 0.3, fontSize: 13, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.mono });
559
- txt(slide, pt.t, { x: sx + 0.7, y: py + 0.1, w: CW - sx - MX - 0.7, h: 0.3, fontSize: 16, color: pal.ink, align: 'left', bold: true, fontFace: S.fonts.heading });
560
- txt(slide, pt.d || '', { x: sx + 0.7, y: py + 0.42, w: CW - sx - MX - 0.7, h: 0.5, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
561
- py += 0.95;
562
- });
563
- folio(slide, p, pal);
564
- }
565
- // =====================================================================
566
- // 12. closing · 封底结语(ink)
567
- // =====================================================================
568
- function renderClosing(slide, p) {
569
- const pal = P('theme09_closing_v1');
570
- slide.background = { color: pal.bg };
571
- tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => { });
572
- slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: '000000', transparency: 35 } });
573
- slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.bg, transparency: 30 } });
574
- if (p.mark) {
575
- const w = 0.3 + p.mark.length * 0.12;
576
- slide.addShape('rect', { x: 5 - w / 2, y: 1.3, w, h: 0.36, fill: { color: pal.accent } });
577
- txt(slide, p.mark, { x: 5 - w / 2, y: 1.3, w, h: 0.36, fontSize: 12, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2 });
578
- }
579
- title(slide, p.word, pal, { x: 1, y: 2.0, w: 8, size: 48, align: 'center', color: pal.ink });
580
- txt(slide, p.subtitle, { x: 1.5, y: 3.3, w: 7, h: 1.0, fontSize: 15, color: pal.ink2, align: 'center', fontFace: S.fonts.body, valign: 'top' });
581
- const col = (p.colophon || []).slice(0, 4);
582
- txt(slide, col.join(' · '), { x: 1, y: 4.7, w: 8, h: 0.3, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.mono, charSpacing: 1 });
583
- }
584
- // =====================================================================
585
- // 注册
586
- // =====================================================================
587
- // theme09 P3 版式 PPTX 渲染函数(临时片段,由 _tmp_splice_p3.mjs 注入 theme09-pptx.ts)
588
- // ────────────────────────────────────────────────────────────────
589
- // P3 通用辅助
590
- // ────────────────────────────────────────────────────────────────
591
- // ────────────────────────────────────────────────────────────────
592
- // P3 渲染器 v2(字段名对齐 Schema;标量数组兼容 {item}/string)
593
- // ────────────────────────────────────────────────────────────────
594
- function t9N(v) {
595
- if (typeof v === 'number')
596
- return v;
597
- const n = parseFloat(String(v == null ? '' : v));
598
- return Number.isFinite(n) ? n : 0;
599
- }
600
- /** 兼容标量数组项:纯字符串 或 {item}/{name}/{label} 对象。 */
601
- function sc(it) {
602
- if (it == null)
603
- return '';
604
- if (typeof it === 'string')
605
- return it;
606
- return String(it.item ?? it.name ?? it.label ?? it.text ?? it.value ?? '');
607
- }
608
- function t9Head(slide, p, pal, o = {}) {
609
- masthead(slide, p, pal);
610
- const hasK = !!p.kicker;
611
- if (hasK)
612
- kicker(slide, p.kicker, pal, { x: MX, y: 1.46 });
613
- const ty = hasK ? 1.82 : 1.66;
614
- const size = o.size ?? 27;
615
- title(slide, p.title, pal, { x: MX, y: ty, w: o.titleW ?? CW, size });
616
- let cy = ty + (size / 72) * 1.2 + 0.12;
617
- if (o.sub !== false && p.subtitle) {
618
- txt(slide, p.subtitle, { x: MX, y: cy, w: o.titleW ?? CW, h: 0.5, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
619
- cy += 0.58;
620
- }
621
- return Math.max(cy, 2.92);
622
- }
623
- function t9Pill(slide, text, x, y, w, h, bg, fg) {
624
- slide.addShape('roundRect', { x, y, w, h, rectRadius: h / 2, fill: { color: bg }, line: { color: bg, width: 0 } });
625
- txt(slide, text, { x, y, w, h, fontSize: 9.5, color: fg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono });
626
- }
627
- /** 等级文本(高/中/低)映射为 0-100 坐标;已是数字则直接用。 */
628
- function lvl(v) {
629
- const s = String(v ?? '').trim();
630
- if (s === '高' || s === 'high')
631
- return 82;
632
- if (s === '中' || s === 'mid')
633
- return 50;
634
- if (s === '低' || s === 'low')
635
- return 18;
636
- const n = t9N(v);
637
- return Number.isFinite(n) && (n > 0 && n <= 100) ? n : 50;
638
- }
639
- // 1 cross-perspective(ink)
640
- function renderCrossPerspective(slide, p) {
641
- const pal = P('theme09_cross-perspective_v1');
642
- frame(slide, pal, { bottom: false });
643
- const y0 = t9Head(slide, p, pal, { size: 26 });
644
- const dims = (p.dimensions || []).slice(0, 4);
645
- const leftW = 6.0;
646
- const conclX = MX + leftW + 0.3;
647
- const conclW = CW - leftW - 0.3;
648
- const rh = Math.min(0.62, (BOT - y0 - 0.1) / Math.max(dims.length, 1));
649
- dims.forEach((d, i) => {
650
- const ry = y0 + i * rh;
651
- const parts = String(d.metrics || '').split('|').map((s) => {
652
- const idx = s.indexOf(':');
653
- const name = idx >= 0 ? s.slice(0, idx).trim() : s.trim();
654
- const val = idx >= 0 ? t9N(s.slice(idx + 1)) : 0;
655
- return { name, val };
656
- });
657
- const maxV = Math.max(1, ...parts.map((x) => x.val));
658
- txt(slide, sc(d.label), { x: MX, y: ry, w: 1.5, h: rh - 0.06, fontSize: 11, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.body });
659
- const bx = MX + 1.6;
660
- const bw = leftW - 1.6;
661
- let acc = 0;
662
- parts.forEach((pt, si) => {
663
- const w = (pt.val / maxV) * bw;
664
- slide.addShape('rect', { x: bx + acc, y: ry + 0.06, w: Math.max(0.02, w - 0.03), h: rh - 0.14, fill: { color: series(pal)[si % 6] }, line: { width: 0 } });
665
- acc += w;
666
- });
667
- txt(slide, sc(d.note), { x: bx, y: ry + rh - 0.2, w: bw, h: 0.18, fontSize: 8.5, color: pal.ink3, italic: true, fontFace: S.fonts.body });
668
- });
669
- card(slide, conclX, y0, conclW, BOT - y0 - 0.1, pal);
670
- txt(slide, '结论', { x: conclX + 0.18, y: y0 + 0.16, w: conclW - 0.36, h: 0.3, fontSize: 12, color: pal.accent, bold: true, fontFace: S.fonts.mono });
671
- txt(slide, p.conclusion, { x: conclX + 0.18, y: y0 + 0.55, w: conclW - 0.36, h: BOT - y0 - 0.8, fontSize: 12, color: pal.ink, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.25 });
672
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
673
- }
674
- // 2 thesis(paper)
675
- function renderThesis(slide, p) {
676
- const pal = P('theme09_thesis_v1');
677
- frame(slide, pal, { bottom: false });
678
- const y0 = t9Head(slide, p, pal, { size: 26 });
679
- const steps = (p.steps || []).slice(0, 3);
680
- const n = Math.max(steps.length, 1);
681
- const gap = 0.25;
682
- const cw = (CW - gap * (n - 1)) / n;
683
- steps.forEach((s, i) => {
684
- const cx = MX + i * (cw + gap);
685
- card(slide, cx, y0, cw, 1.7, pal);
686
- t9Pill(slide, String(i + 1), cx + 0.18, y0 + 0.18, 0.42, 0.42, pal.accent, 'FFFFFF');
687
- txt(slide, sc(s.claim), { x: cx + 0.72, y: y0 + 0.18, w: cw - 0.9, h: 0.5, fontSize: 13, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading });
688
- txt(slide, sc(s.evidence), { x: cx + 0.18, y: y0 + 0.72, w: cw - 0.36, h: 0.9, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
689
- });
690
- const by = y0 + 1.95;
691
- if (p.conclusion) {
692
- slide.addShape('rect', { x: MX, y: by, w: 0.08, h: BOT - by - 0.2, fill: { color: pal.accent } });
693
- txt(slide, p.conclusion, { x: MX + 0.22, y: by, w: CW - 0.22, h: BOT - by - 0.2, fontSize: 12.5, color: pal.ink, valign: 'top', bold: true, fontFace: S.fonts.body, lineSpacingMultiple: 1.25 });
694
- }
695
- folio(slide, p, pal);
696
- }
697
- // 3 value-chain(paper)
698
- function renderValueChain(slide, p) {
699
- const pal = P('theme09_value-chain_v1');
700
- frame(slide, pal, { bottom: false });
701
- const y0 = t9Head(slide, p, pal, { size: 26 });
702
- const cols = [
703
- { label: p.upstreamLabel, items: p.upstream },
704
- { label: p.midstreamLabel, items: p.midstream },
705
- { label: p.downstreamLabel, items: p.downstream },
706
- ];
707
- const gap = 0.3;
708
- const cw = (CW - gap * 2) / 3;
709
- const ch = BOT - y0 - 0.1;
710
- cols.forEach((c, i) => {
711
- const cx = MX + i * (cw + gap);
712
- card(slide, cx, y0, cw, ch, pal);
713
- slide.addShape('rect', { x: cx, y: y0, w: cw, h: 0.42, fill: { color: series(pal)[i] } });
714
- txt(slide, String(c.label || ''), { x: cx + 0.15, y: y0, w: cw - 0.3, h: 0.42, fontSize: 12, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.heading });
715
- const items = (c.items || []).slice(0, 4);
716
- let iy = y0 + 0.55;
717
- items.forEach((it) => {
718
- txt(slide, sc(it.name), { x: cx + 0.15, y: iy, w: cw - 0.3, h: 0.26, fontSize: 11.5, color: pal.ink, bold: true, fontFace: S.fonts.body });
719
- txt(slide, sc(it.desc), { x: cx + 0.15, y: iy + 0.26, w: cw - 0.3, h: 0.4, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
720
- iy += 0.74;
721
- });
722
- });
723
- folio(slide, p, pal);
724
- }
725
- // 4 risk(ink)
726
- function renderRisk(slide, p) {
727
- const pal = P('theme09_risk_v1');
728
- frame(slide, pal, { bottom: false });
729
- const y0 = t9Head(slide, p, pal, { size: 26 });
730
- const gx = MX + 1.3;
731
- const gy = y0 + 0.2;
732
- const gw = 6.4;
733
- const gh = BOT - y0 - 0.9;
734
- card(slide, gx, gy, gw, gh, pal);
735
- // quadrant lines at 50%
736
- slide.addShape('line', { x: gx + gw / 2, y: gy, w: 0, h: gh, line: { color: pal.borderStrong, width: 0.75, dashType: 'dash' } });
737
- slide.addShape('line', { x: gx, y: gy + gh / 2, w: gw, h: 0, line: { color: pal.borderStrong, width: 0.75, dashType: 'dash' } });
738
- txt(slide, p.axisProbability, { x: gx, y: gy + gh + 0.02, w: gw, h: 0.2, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
739
- txt(slide, p.axisImpact, { x: gx - 1.25, y: gy, w: 1.1, h: gh, fontSize: 9, color: pal.ink3, align: 'center', valign: 'middle', rotate: 270, fontFace: S.fonts.mono });
740
- (p.risks || []).slice(0, 8).forEach((r) => {
741
- const x = gx + (lvl(r.probability) / 100) * gw;
742
- const y = gy + (1 - lvl(r.impact) / 100) * gh;
743
- const col = r.level === '高' ? pal.accent : r.level === '中' ? pal.accent2 : series(pal)[3];
744
- slide.addShape('ellipse', { x: x - 0.11, y: y - 0.11, w: 0.22, h: 0.22, fill: { color: col }, line: { color: pal.bg, width: 1 } });
745
- txt(slide, sc(r.name), { x: x - 0.9, y: y - 0.32, w: 1.8, h: 0.2, fontSize: 8.5, color: pal.ink, align: 'center', fontFace: S.fonts.body });
746
- });
747
- folio(slide, p, pal);
748
- }
749
- // 5 outlook(ink)
750
- function renderOutlook(slide, p) {
751
- const pal = P('theme09_outlook_v1');
752
- frame(slide, pal, { bottom: false });
753
- const y0 = t9Head(slide, p, pal, { size: 26 });
754
- const items = (p.outlooks || []).slice(0, 3);
755
- const n = Math.max(items.length, 1);
756
- const gap = 0.3;
757
- const cw = (CW - gap * (n - 1)) / n;
758
- items.forEach((o, i) => {
759
- const cx = MX + i * (cw + gap);
760
- card(slide, cx, y0, cw, 2.5, pal);
761
- txt(slide, sc(o.title), { x: cx + 0.18, y: y0 + 0.18, w: cw - 0.36, h: 0.5, fontSize: 13, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading });
762
- txt(slide, sc(o.description), { x: cx + 0.18, y: y0 + 0.75, w: cw - 0.36, h: 1.4, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
763
- const trend = String(o.trend || '');
764
- const tc = trend.includes('上行') ? series(pal)[3] : trend.includes('下行') ? pal.accent : pal.accent2;
765
- t9Pill(slide, trend, cx + 0.18, y0 + 2.1, cw - 0.36, 0.32, tc, 'FFFFFF');
766
- });
767
- folio(slide, p, pal);
768
- }
769
- // 6 conclusion(paper)
770
- function renderConclusion(slide, p) {
771
- const pal = P('theme09_conclusion_v1');
772
- frame(slide, pal, { bottom: false });
773
- const y0 = t9Head(slide, p, pal, { size: 26 });
774
- if (p.showBand)
775
- colorBar(slide, MX, y0 - 0.02, CW, pal);
776
- const items = (p.conclusions || []).slice(0, 4);
777
- const n = Math.max(items.length, 1);
778
- const gap = 0.22;
779
- const cw = (CW - gap * (n - 1)) / n;
780
- items.forEach((c, i) => {
781
- const cx = MX + i * (cw + gap);
782
- txt(slide, String(i + 1).padStart(2, '0'), { x: cx, y: y0 + 0.12, w: cw, h: 0.4, fontSize: 22, color: pal.accent, bold: true, align: 'left', fontFace: S.fonts.mono });
783
- txt(slide, sc(c.title), { x: cx, y: y0 + 0.6, w: cw - 0.1, h: 0.5, fontSize: 12.5, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading });
784
- txt(slide, sc(c.summary), { x: cx, y: y0 + 1.1, w: cw - 0.1, h: 1.6, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
785
- });
786
- folio(slide, p, pal);
787
- }
788
- // 7 bracket(paper)
789
- function renderBracket(slide, p) {
790
- const pal = P('theme09_bracket_v1');
791
- frame(slide, pal, { bottom: false });
792
- const y0 = t9Head(slide, p, pal, { size: 26 });
793
- const items = (p.items || []).slice(0, 6);
794
- const n = Math.max(items.length, 1);
795
- const iw = Math.min(1.7, (CW - 0.6) / n);
796
- const gap = (CW - iw * n) / (n + 1);
797
- items.forEach((it, i) => {
798
- const ix = MX + gap + i * (iw + gap);
799
- card(slide, ix, y0, iw, 0.7, pal);
800
- txt(slide, sc(it.item), { x: ix + 0.05, y: y0, w: iw - 0.1, h: 0.7, fontSize: 10.5, color: pal.ink, bold: true, align: 'center', valign: 'middle', fontFace: S.fonts.body });
801
- if (i < n - 1)
802
- slide.addShape('line', { x: ix + iw, y: y0 + 0.35, w: gap, h: 0, line: { color: pal.borderStrong, width: 1, endArrowType: 'triangle' } });
803
- });
804
- const sy = y0 + 1.0;
805
- slide.addShape('line', { x: MX + gap, y: sy, w: 0, h: 0.5, line: { color: pal.accent, width: 2 } });
806
- slide.addShape('line', { x: MX + gap, y: sy + 0.5, w: CW - 2 * gap, h: 0, line: { color: pal.accent, width: 2 } });
807
- slide.addShape('line', { x: MX + CW - gap, y: sy, w: 0, h: 0.5, line: { color: pal.accent, width: 2 } });
808
- txt(slide, p.summary, { x: MX, y: sy + 0.7, w: CW, h: BOT - sy - 0.9, fontSize: 14, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading, lineSpacingMultiple: 1.3 });
809
- folio(slide, p, pal);
810
- }
811
- // 8 flow(ink)
812
- function renderFlow(slide, p) {
813
- const pal = P('theme09_flow_v1');
814
- frame(slide, pal, { bottom: false });
815
- const y0 = t9Head(slide, p, pal);
816
- const nodes = (p.nodes || []).slice(0, 6);
817
- const links = (p.links || []).slice(0, 8);
818
- const colW = (CW - 1.0) / Math.max(nodes.length, 1);
819
- const nodeBoxes = [];
820
- nodes.forEach((nd, i) => {
821
- const bx = MX + 0.2 + i * colW;
822
- const by = y0 + 0.3 + (i % 2) * 0.9;
823
- const bw = colW - 0.3;
824
- nodeBoxes.push({ x: bx, y: by, w: bw, h: 0.5, name: sc(nd.name) });
825
- card(slide, bx, by, bw, 0.5, pal, pal.surfaceElevated);
826
- txt(slide, sc(nd.name), { x: bx + 0.05, y: by, w: bw - 0.1, h: 0.5, fontSize: 10, color: pal.ink, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.body });
827
- });
828
- links.forEach((lk, i) => {
829
- const a = nodeBoxes[Math.min(t9N(lk.source), nodeBoxes.length - 1)];
830
- const b = nodeBoxes[Math.min(t9N(lk.target), nodeBoxes.length - 1)];
831
- if (!a || !b)
832
- return;
833
- const v = t9N(lk.value);
834
- const w = Math.max(0.5, Math.min(4, v / 30));
835
- slide.addShape('line', { x: a.x + a.w, y: a.y + a.h / 2, w: b.x - (a.x + a.w), h: b.y + b.h / 2 - (a.y + a.h / 2), line: { color: series(pal)[i % 6], width: w, endArrowType: 'triangle' } });
836
- });
837
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
838
- }
839
- // 9 orbit(ink)
840
- function renderOrbit(slide, p) {
841
- const pal = P('theme09_orbit_v1');
842
- frame(slide, pal, { bottom: false });
843
- const y0 = t9Head(slide, p, pal, { size: 26 });
844
- const cx = MX + CW / 2;
845
- const cy = (y0 + BOT) / 2 + 0.1;
846
- const R = Math.min((BOT - y0) / 2 - 0.3, CW / 2 - 1.0);
847
- slide.addShape('ellipse', { x: cx - R, y: cy - R, w: R * 2, h: R * 2, fill: { color: pal.surface }, line: { color: pal.borderStrong, width: 1.25 } });
848
- slide.addShape('ellipse', { x: cx - 0.9, y: cy - 0.9, w: 1.8, h: 1.8, fill: { color: pal.accent }, line: { width: 0 } });
849
- txt(slide, p.centerText, { x: cx - 0.9, y: cy - 0.9, w: 1.8, h: 1.8, fontSize: 12, color: 'FFFFFF', align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading });
850
- const evs = (p.events || []).slice(0, 8);
851
- const n = Math.max(evs.length, 1);
852
- evs.forEach((e, i) => {
853
- const ang = (Math.PI * 2 * i) / n - Math.PI / 2;
854
- const ex = cx + Math.cos(ang) * R;
855
- const ey = cy + Math.sin(ang) * R;
856
- slide.addShape('ellipse', { x: ex - 0.09, y: ey - 0.09, w: 0.18, h: 0.18, fill: { color: series(pal)[i % 6] }, line: { color: pal.bg, width: 1 } });
857
- const lx = cx + Math.cos(ang) * (R + 0.35);
858
- const ly = cy + Math.sin(ang) * (R + 0.35);
859
- txt(slide, sc(e.year), { x: lx - 0.5, y: ly - 0.34, w: 1.0, h: 0.18, fontSize: 9, color: pal.accent, align: 'center', bold: true, fontFace: S.fonts.mono });
860
- txt(slide, sc(e.label), { x: lx - 0.6, y: ly - 0.16, w: 1.2, h: 0.5, fontSize: 9, color: pal.ink, align: 'center', valign: 'top', fontFace: S.fonts.body });
861
- });
862
- folio(slide, p, pal);
863
- }
864
- // 10 vertical(paper)
865
- function renderVertical(slide, p) {
866
- const pal = P('theme09_vertical_v1');
867
- frame(slide, pal, { bottom: false });
868
- const y0 = t9Head(slide, p, pal, { size: 26 });
869
- const cards = (p.cards || []).slice(0, 3);
870
- const n = Math.max(cards.length, 1);
871
- const gap = 0.3;
872
- const cw = (CW - gap * (n - 1)) / n;
873
- cards.forEach((c, i) => {
874
- const cx = MX + i * (cw + gap);
875
- card(slide, cx, y0, cw, 2.5, pal);
876
- slide.addShape('rect', { x: cx, y: y0, w: cw, h: 1.0, fill: { color: pal.surfaceElevated }, line: { color: pal.border, width: 1 } });
877
- txt(slide, '影像', { x: cx, y: y0, w: cw, h: 1.0, fontSize: 10, color: pal.ink3, align: 'center', valign: 'middle', italic: true, fontFace: S.fonts.body });
878
- txt(slide, sc(c.title), { x: cx + 0.18, y: y0 + 1.12, w: cw - 0.36, h: 0.4, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
879
- txt(slide, sc(c.description), { x: cx + 0.18, y: y0 + 1.55, w: cw - 0.36, h: 0.85, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
880
- });
881
- folio(slide, p, pal);
882
- }
883
- // 11 calendar(paper)
884
- function renderCalendar(slide, p) {
885
- const pal = P('theme09_calendar_v1');
886
- frame(slide, pal, { bottom: false });
887
- const y0 = t9Head(slide, p, pal, { size: 26 });
888
- const year = t9N(p.year) || 2026;
889
- const month = Math.max(1, Math.min(12, t9N(p.month) || 8));
890
- const days = new Date(year, month, 0).getDate();
891
- const cols = 7;
892
- const rows = Math.ceil(days / cols);
893
- const gridX = MX + 1.4;
894
- const gridW = CW - 1.4;
895
- const cell = gridW / cols;
896
- const gridH = (BOT - y0 - 0.2);
897
- const cellH = Math.min(0.55, gridH / rows);
898
- const evByDate = {};
899
- (p.events || []).forEach((e) => { (evByDate[String(sc(e.date))] = evByDate[String(sc(e.date))] || []).push(e); });
900
- for (let d = 1; d <= days; d++) {
901
- const r = Math.floor((d - 1) / cols);
902
- const c = (d - 1) % cols;
903
- const x = gridX + c * cell;
904
- const y = y0 + r * cellH;
905
- const has = evByDate[String(d)];
906
- slide.addShape('rect', { x, y, w: cell - 0.03, h: cellH - 0.03, fill: { color: has ? pal.accent : pal.surfaceElevated }, line: { color: pal.border, width: 0.75 } });
907
- txt(slide, String(d), { x: x + 0.04, y: y + 0.02, w: cell - 0.1, h: 0.18, fontSize: 9, color: has ? 'FFFFFF' : pal.ink3, bold: true, fontFace: S.fonts.mono });
908
- if (has)
909
- txt(slide, String(sc(has[0].name)).slice(0, 6), { x: x + 0.04, y: y + 0.2, w: cell - 0.08, h: cellH - 0.24, fontSize: 7, color: 'FFFFFF', valign: 'top', fontFace: S.fonts.body });
910
- }
911
- txt(slide, year + '.' + String(month).padStart(2, '0'), { x: MX, y: y0, w: 1.3, h: 0.4, fontSize: 16, color: pal.ink, bold: true, fontFace: S.fonts.mono });
912
- folio(slide, p, pal);
913
- }
914
- // 12 phases(ink)
915
- function renderPhases(slide, p) {
916
- const pal = P('theme09_phases_v1');
917
- frame(slide, pal, { bottom: false });
918
- const y0 = t9Head(slide, p, pal, { size: 26 });
919
- const phases = (p.phases || []).slice(0, 4);
920
- const n = Math.max(phases.length, 1);
921
- const gap = 0.25;
922
- const cw = (CW - gap * (n - 1)) / n;
923
- phases.forEach((ph, i) => {
924
- const cx = MX + i * (cw + gap);
925
- card(slide, cx, y0, cw, 2.6, pal);
926
- t9Pill(slide, String(i + 1), cx + 0.18, y0 + 0.18, 0.4, 0.4, series(pal)[i % 6], 'FFFFFF');
927
- txt(slide, sc(ph.name), { x: cx + 0.66, y: y0 + 0.18, w: cw - 0.8, h: 0.4, fontSize: 12, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.heading });
928
- txt(slide, [sc(ph.start), sc(ph.end)].filter(Boolean).join(' – '), { x: cx + 0.18, y: y0 + 0.66, w: cw - 0.36, h: 0.2, fontSize: 9, color: pal.accent, fontFace: S.fonts.mono });
929
- const ms = (ph.milestones || []).slice(0, 3);
930
- let my = y0 + 0.95;
931
- ms.forEach((m) => {
932
- txt(slide, '• ' + sc(m), { x: cx + 0.18, y: my, w: cw - 0.36, h: 0.5, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
933
- my += 0.52;
934
- });
935
- });
936
- folio(slide, p, pal);
937
- }
938
- // 13 gauge(ink)
939
- function renderGauge(slide, p) {
940
- const pal = P('theme09_gauge_v1');
941
- frame(slide, pal, { bottom: false });
942
- const y0 = t9Head(slide, p, pal);
943
- const min = t9N(p.min) || 0;
944
- const max = t9N(p.max) || 100;
945
- const val = t9N(p.value);
946
- const span = Math.max(1, max - min);
947
- const gx = MX + 0.3;
948
- const gw = 6.0;
949
- const gy = y0 + 0.3;
950
- const gh = 0.5;
951
- (p.ranges || []).forEach((rg) => {
952
- const a = (t9N(rg.min) - min) / span;
953
- const b = (t9N(rg.max) - min) / span;
954
- const col = rg.color === '绿' ? series(pal)[3] : rg.color === '黄' ? series(pal)[2] : pal.accent;
955
- slide.addShape('rect', { x: gx + a * gw, y: gy, w: Math.max(0.02, (b - a) * gw), h: gh, fill: { color: col }, line: { width: 0 } });
956
- });
957
- const xp = gx + ((val - min) / span) * gw;
958
- slide.addShape('line', { x: xp, y: gy - 0.15, w: 0, h: gh + 0.3, line: { color: pal.ink, width: 2.5 } });
959
- txt(slide, p.readingLabel, { x: gx, y: gy + gh + 0.05, w: gw, h: 0.2, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
960
- txt(slide, val.toFixed(1) + (p.unit ? ' ' + p.unit : ''), { x: gx + gw + 0.3, y: y0 + 0.1, w: CW - gw - 0.6, h: 0.9, fontSize: 40, color: pal.accent, bold: true, align: 'left', valign: 'middle', fontFace: S.fonts.heading });
961
- txt(slide, p.subtitle, { x: gx + gw + 0.3, y: y0 + 1.0, w: CW - gw - 0.6, h: 1.2, fontSize: 11, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.2 });
962
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
963
- }
964
- // 14 scoreboard(ink)
965
- function renderScoreboard(slide, p) {
966
- const pal = P('theme09_scoreboard_v1');
967
- frame(slide, pal, { bottom: false });
968
- const y0 = t9Head(slide, p, pal);
969
- const headers = (p.headers || []).slice(0, 5);
970
- const rows = (p.rows || []).slice(0, 6);
971
- const hn = Math.max(headers.length, 1);
972
- const nameW = 2.2;
973
- const tblW = CW - nameW;
974
- const colW = tblW / hn;
975
- const rh = 0.42;
976
- // header
977
- slide.addShape('rect', { x: MX, y: y0, w: nameW, h: rh, fill: { color: pal.accent } });
978
- txt(slide, '参赛者', { x: MX + 0.1, y: y0, w: nameW - 0.2, h: rh, fontSize: 10, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.body });
979
- headers.forEach((h, i) => {
980
- const hx = MX + nameW + i * colW;
981
- slide.addShape('rect', { x: hx, y: y0, w: colW, h: rh, fill: { color: pal.surfaceElevated } });
982
- txt(slide, sc(h.label), { x: hx + 0.04, y: y0, w: colW - 0.08, h: rh, fontSize: 9, color: pal.ink, bold: true, align: 'center', valign: 'middle', fontFace: S.fonts.body });
983
- });
984
- rows.forEach((r, ri) => {
985
- const ry = y0 + (ri + 1) * rh;
986
- const hl = String(r.name) === String(p.highlightRow);
987
- slide.addShape('rect', { x: MX, y: ry, w: nameW, h: rh, fill: { color: hl ? pal.accent : pal.surface } });
988
- txt(slide, sc(r.name), { x: MX + 0.1, y: ry, w: nameW - 0.2, h: rh, fontSize: 10, color: hl ? 'FFFFFF' : pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.body });
989
- (r.scores || []).slice(0, hn).forEach((s, i) => {
990
- const hx = MX + nameW + i * colW;
991
- txt(slide, sc(s), { x: hx + 0.04, y: ry, w: colW - 0.08, h: rh, fontSize: 10, color: pal.ink2, align: 'center', valign: 'middle', fontFace: S.fonts.mono });
992
- });
993
- });
994
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
995
- }
996
- // 15 trend(paper)
997
- function renderTrend(slide, p) {
998
- const pal = P('theme09_trend_v1');
999
- frame(slide, pal, { bottom: false });
1000
- const y0 = t9Head(slide, p, pal);
1001
- const qs = (p.quarters || []).map(sc);
1002
- const main = (p.mainSeries || []).map(t9N);
1003
- const yoy = (p.yoySeries || []).map(t9N);
1004
- const n = Math.max(qs.length, 1);
1005
- const plotX = MX + 0.3;
1006
- const plotW = CW - 1.0;
1007
- const plotY = y0 + 0.2;
1008
- const plotH = 2.7;
1009
- const maxM = Math.max(1, ...main);
1010
- const baseY = plotY + plotH;
1011
- const bw = (plotW / n) * 0.5;
1012
- qs.forEach((q, i) => {
1013
- const cx = plotX + (i + 0.5) * (plotW / n);
1014
- const h = (main[i] / maxM) * (plotH - 0.3);
1015
- slide.addShape('rect', { x: cx - bw / 2, y: baseY - h, w: bw, h, fill: { color: pal.accent }, line: { width: 0 } });
1016
- txt(slide, q, { x: cx - plotW / n / 2, y: baseY + 0.02, w: plotW / n, h: 0.2, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1017
- });
1018
- // yoy line (right axis, scaled to -20..40)
1019
- const ly = (v) => baseY - ((v + 20) / 60) * (plotH - 0.3);
1020
- let prev = null;
1021
- qs.forEach((_q, i) => {
1022
- const cx = plotX + (i + 0.5) * (plotW / n);
1023
- const py = ly(yoy[i]);
1024
- if (prev)
1025
- slide.addShape('line', { x: prev.x, y: prev.y, w: cx - prev.x, h: py - prev.y, line: { color: pal.accent2, width: 2 } });
1026
- prev = { x: cx, y: py };
1027
- });
1028
- t9Pill(slide, p.mainName + ' · ' + (p.unit || ''), plotX, plotY - 0.05, 2.4, 0.26, pal.accent, 'FFFFFF');
1029
- t9Pill(slide, p.yoyName, plotX + 2.6, plotY - 0.05, 2.2, 0.26, pal.accent2, 'FFFFFF');
1030
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1031
- }
1032
- // 16 histogram(paper)
1033
- function renderHistogram(slide, p) {
1034
- const pal = P('theme09_histogram_v1');
1035
- frame(slide, pal, { bottom: false });
1036
- const y0 = t9Head(slide, p, pal);
1037
- const bins = (p.bins || []).slice(0, 12);
1038
- const n = Math.max(bins.length, 1);
1039
- const plotX = MX;
1040
- const plotW = CW - 1.2;
1041
- const plotY = y0 + 0.2;
1042
- const plotH = 2.7;
1043
- const maxC = Math.max(1, ...bins.map((b) => t9N(b.count)));
1044
- const baseY = plotY + plotH;
1045
- const bw = plotW / n;
1046
- bins.forEach((b, i) => {
1047
- const h = (t9N(b.count) / maxC) * (plotH - 0.3);
1048
- slide.addShape('rect', { x: plotX + i * bw + 0.04, y: baseY - h, w: bw - 0.08, h, fill: { color: series(pal)[i % 6] }, line: { width: 0 } });
1049
- txt(slide, String(sc(b.range)).slice(0, 6), { x: plotX + i * bw, y: baseY + 0.02, w: bw, h: 0.2, fontSize: 8, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1050
- });
1051
- if (p.mean != null) {
1052
- const mx = plotX + (t9N(p.mean) / Math.max(1, ...bins.map((b) => t9N(b.count)))) * plotW;
1053
- slide.addShape('line', { x: mx, y: plotY, w: 0, h: plotH, line: { color: pal.accent, width: 2, dashType: 'dash' } });
1054
- txt(slide, '均值 ' + p.mean, { x: Math.min(mx + 0.05, plotX + plotW - 1.5), y: plotY + 0.02, w: 1.5, h: 0.2, fontSize: 9, color: pal.accent, bold: true, fontFace: S.fonts.mono });
1055
- }
1056
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1057
- }
1058
- // 17 forecast-fan(ink)
1059
- function renderForecastFan(slide, p) {
1060
- const pal = P('theme09_forecast-fan_v1');
1061
- frame(slide, pal, { bottom: false });
1062
- const y0 = t9Head(slide, p, pal);
1063
- const seriesArr = (p.series || []).slice(0, 3);
1064
- const cats = Math.max(1, ...seriesArr.map((s) => String(s.data || '').split('|').length));
1065
- const plotX = MX + 0.3;
1066
- const plotW = CW - 1.0;
1067
- const plotY = y0 + 0.2;
1068
- const plotH = 2.6;
1069
- const baseY = plotY + plotH;
1070
- const all = seriesArr.flatMap((s) => String(s.data || '').split('|').map(t9N));
1071
- const maxV = Math.max(1, ...all);
1072
- const xAt = (i) => plotX + ((i + 0.5) / cats) * plotW;
1073
- const yAt = (v) => baseY - (v / maxV) * (plotH - 0.3);
1074
- // draw outer two series as a filled band (fan) via thin rects
1075
- if (seriesArr.length >= 2) {
1076
- const lo = String(seriesArr[0].data || '').split('|').map(t9N);
1077
- const hi = String(seriesArr[seriesArr.length - 1].data || '').split('|').map(t9N);
1078
- lo.forEach((v, i) => {
1079
- const yLo = yAt(Math.min(v, hi[i]));
1080
- const yHi = yAt(Math.max(v, hi[i]));
1081
- slide.addShape('rect', { x: xAt(i) - 0.12, y: yHi, w: 0.24, h: Math.max(0.02, yLo - yHi), fill: { color: pal.accent, transparency: 78 }, line: { width: 0 } });
1082
- });
1083
- }
1084
- // each series as line
1085
- seriesArr.forEach((s, si) => {
1086
- const vals = String(s.data || '').split('|').map(t9N);
1087
- let prev = null;
1088
- vals.forEach((v, i) => {
1089
- const px = xAt(i), py = yAt(v);
1090
- if (prev)
1091
- slide.addShape('line', { x: prev.x, y: prev.y, w: px - prev.x, h: py - prev.y, line: { color: series(pal)[si % 6], width: 2 } });
1092
- prev = { x: px, y: py };
1093
- });
1094
- });
1095
- (p.annotations || []).slice(0, 3).forEach((a, i) => {
1096
- const ax = plotX + (t9N(a.period) / cats) * plotW;
1097
- txt(slide, sc(a.text), { x: Math.min(ax, plotX + plotW - 2), y: plotY + 0.05 + i * 0.22, w: 2.0, h: 0.2, fontSize: 8.5, color: pal.ink2, italic: true, fontFace: S.fonts.body });
1098
- });
1099
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1100
- }
1101
- // 18 plans(paper)
1102
- function renderPlans(slide, p) {
1103
- const pal = P('theme09_plans_v1');
1104
- frame(slide, pal, { bottom: false });
1105
- const y0 = t9Head(slide, p, pal, { size: 26 });
1106
- const plans = (p.plans || []).slice(0, 3);
1107
- const n = Math.max(plans.length, 1);
1108
- const gap = 0.3;
1109
- const cw = (CW - gap * (n - 1)) / n;
1110
- plans.forEach((pl, i) => {
1111
- const cx = MX + i * (cw + gap);
1112
- const rec = !!pl.recommended;
1113
- card(slide, cx, y0, cw, 2.7, pal, rec ? pal.accent : pal.surfaceElevated, rec ? pal.accent : pal.border);
1114
- txt(slide, sc(pl.name), { x: cx + 0.18, y: y0 + 0.18, w: cw - 0.36, h: 0.4, fontSize: 13, color: rec ? pal.accent : pal.ink, bold: true, fontFace: S.fonts.heading });
1115
- txt(slide, sc(pl.summary), { x: cx + 0.18, y: y0 + 0.62, w: cw - 0.36, h: 0.5, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1116
- (pl.points || []).slice(0, 3).forEach((pt, k) => {
1117
- txt(slide, '– ' + sc(pt), { x: cx + 0.18, y: y0 + 1.15 + k * 0.42, w: cw - 0.36, h: 0.4, fontSize: 9.5, color: pal.ink, valign: 'top', fontFace: S.fonts.body });
1118
- });
1119
- if (rec)
1120
- t9Pill(slide, '推荐', cx + cw - 1.0, y0 + 0.2, 0.82, 0.3, pal.accent, 'FFFFFF');
1121
- });
1122
- if (p.conclusion)
1123
- txt(slide, p.conclusion, { x: MX, y: y0 + 2.85, w: CW, h: 0.5, fontSize: 11, color: pal.ink, bold: true, fontFace: S.fonts.body });
1124
- folio(slide, p, pal);
1125
- }
1126
- // 19 stair(ink)
1127
- function renderStair(slide, p) {
1128
- const pal = P('theme09_stair_v1');
1129
- frame(slide, pal, { bottom: false });
1130
- const y0 = t9Head(slide, p, pal, { size: 26 });
1131
- const steps = (p.steps || []).slice(0, 5);
1132
- const n = Math.max(steps.length, 1);
1133
- const stepW = (CW - 0.6) / n;
1134
- const baseY = BOT - 0.3;
1135
- const stepH = (baseY - y0 - 0.3) / n;
1136
- steps.forEach((s, i) => {
1137
- const sx = MX + i * stepW + 0.1;
1138
- const sy = baseY - (i + 1) * stepH;
1139
- card(slide, sx, sy, stepW - 0.2, (i + 1) * stepH, pal, pal.surfaceElevated);
1140
- t9Pill(slide, String(i + 1), sx + 0.12, sy + 0.12, 0.36, 0.36, series(pal)[i % 6], 'FFFFFF');
1141
- txt(slide, sc(s.name), { x: sx + 0.55, y: sy + 0.12, w: stepW - 0.7, h: 0.4, fontSize: 12, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.heading });
1142
- txt(slide, sc(s.description), { x: sx + 0.14, y: sy + 0.55, w: stepW - 0.34, h: (i + 1) * stepH - 0.7, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1143
- });
1144
- folio(slide, p, pal);
1145
- }
1146
- // 20 stacked(paper)
1147
- function renderStacked(slide, p) {
1148
- const pal = P('theme09_stacked_v1');
1149
- frame(slide, pal, { bottom: false });
1150
- const y0 = t9Head(slide, p, pal, { size: 26 });
1151
- const periods = (p.periods || []).map(sc);
1152
- const seriesArr = (p.series || []).slice(0, 6);
1153
- const n = Math.max(periods.length, 1);
1154
- const plotX = MX;
1155
- const plotW = CW;
1156
- const plotY = y0 + 0.2;
1157
- const plotH = 2.4;
1158
- const baseY = plotY + plotH;
1159
- const maxTot = Math.max(1, ...seriesArr.map((s) => String(s.data || '').split('|').reduce((a, x) => a + t9N(x), 0)));
1160
- const bw = (plotW / n) * 0.6;
1161
- periods.forEach((pr, i) => {
1162
- const cx = plotX + (i + 0.5) * (plotW / n);
1163
- let acc = 0;
1164
- seriesArr.forEach((s, si) => {
1165
- const v = t9N(String(s.data || '').split('|')[i]);
1166
- const h = (v / maxTot) * (plotH - 0.2);
1167
- slide.addShape('rect', { x: cx - bw / 2, y: baseY - acc - h, w: bw, h: Math.max(0.01, h), fill: { color: series(pal)[si % 6] }, line: { color: pal.bg, width: 0.5 } });
1168
- acc += h;
1169
- });
1170
- txt(slide, pr, { x: cx - plotW / n / 2, y: baseY + 0.02, w: plotW / n, h: 0.2, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1171
- });
1172
- seriesArr.forEach((s, si) => {
1173
- const lx = MX + (si % 3) * 2.6;
1174
- const ly = baseY + 0.32 + Math.floor(si / 3) * 0.28;
1175
- slide.addShape('rect', { x: lx, y: ly, w: 0.18, h: 0.18, fill: { color: series(pal)[si % 6] } });
1176
- txt(slide, sc(s.name), { x: lx + 0.24, y: ly - 0.02, w: 2.3, h: 0.22, fontSize: 9.5, color: pal.ink2, fontFace: S.fonts.body });
1177
- });
1178
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1179
- }
1180
- // 21 era(ink)
1181
- function renderEra(slide, p) {
1182
- const pal = P('theme09_era_v1');
1183
- frame(slide, pal, { bottom: false });
1184
- const y0 = t9Head(slide, p, pal, { size: 26 });
1185
- const eras = (p.eras || []).slice(0, 3);
1186
- const n = Math.max(eras.length, 1);
1187
- const gap = 0.3;
1188
- const cw = (CW - gap * (n - 1)) / n;
1189
- eras.forEach((e, i) => {
1190
- const cx = MX + i * (cw + gap);
1191
- card(slide, cx, y0, cw, BOT - y0 - 0.1, pal);
1192
- slide.addShape('rect', { x: cx, y: y0, w: 0.1, h: BOT - y0 - 0.1, fill: { color: series(pal)[i % 6] } });
1193
- txt(slide, sc(e.year), { x: cx + 0.22, y: y0 + 0.15, w: cw - 0.4, h: 0.3, fontSize: 16, color: pal.accent, bold: true, fontFace: S.fonts.mono });
1194
- txt(slide, sc(e.label), { x: cx + 0.22, y: y0 + 0.5, w: cw - 0.4, h: 0.5, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
1195
- (e.events || []).slice(0, 4).forEach((ev, k) => {
1196
- txt(slide, '· ' + sc(ev), { x: cx + 0.22, y: y0 + 1.1 + k * 0.42, w: cw - 0.44, h: 0.4, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1197
- });
1198
- });
1199
- folio(slide, p, pal);
1200
- }
1201
- // 22 roadmap(paper)
1202
- function renderRoadmap(slide, p) {
1203
- const pal = P('theme09_roadmap_v1');
1204
- frame(slide, pal, { bottom: false });
1205
- const y0 = t9Head(slide, p, pal, { size: 26 });
1206
- const stages = (p.stages || []).slice(0, 4);
1207
- const n = Math.max(stages.length, 1);
1208
- const gap = 0.3;
1209
- const cw = (CW - gap * (n - 1)) / n;
1210
- const lineY = y0 + 0.5;
1211
- slide.addShape('line', { x: MX, y: lineY, w: CW, h: 0, line: { color: pal.borderStrong, width: 1.5 } });
1212
- stages.forEach((st, i) => {
1213
- const cx = MX + i * (cw + gap);
1214
- const col = st.status === '已完成' ? series(pal)[3] : st.status === '进行中' ? pal.accent : pal.ink3;
1215
- slide.addShape('ellipse', { x: cx + cw / 2 - 0.14, y: lineY - 0.14, w: 0.28, h: 0.28, fill: { color: col }, line: { color: pal.bg, width: 1.5 } });
1216
- card(slide, cx, y0 + 0.95, cw, 1.9, pal);
1217
- txt(slide, sc(st.name), { x: cx + 0.15, y: y0 + 1.1, w: cw - 0.3, h: 0.4, fontSize: 12, color: pal.ink, bold: true, fontFace: S.fonts.heading });
1218
- t9Pill(slide, sc(st.status), cx + 0.15, y0 + 1.5, cw - 0.3, 0.28, col, 'FFFFFF');
1219
- txt(slide, sc(st.time), { x: cx + 0.15, y: y0 + 1.85, w: cw - 0.3, h: 0.2, fontSize: 9, color: pal.accent, fontFace: S.fonts.mono });
1220
- txt(slide, sc(st.description), { x: cx + 0.15, y: y0 + 2.1, w: cw - 0.3, h: 0.7, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1221
- });
1222
- if (p.endCaption)
1223
- txt(slide, p.endCaption, { x: MX, y: y0 + 0.2, w: CW, h: 0.25, fontSize: 10, color: pal.ink3, align: 'right', italic: true, fontFace: S.fonts.body });
1224
- folio(slide, p, pal);
1225
- }
1226
- // 23 score(ink)
1227
- function renderScore(slide, p) {
1228
- const pal = P('theme09_score_v1');
1229
- frame(slide, pal, { bottom: false });
1230
- const y0 = t9Head(slide, p, pal);
1231
- const crit = (p.criteria || []).slice(0, 6);
1232
- const n = Math.max(crit.length, 1);
1233
- const labelW = 2.2;
1234
- const plotX = MX + labelW;
1235
- const plotW = CW - labelW - 1.2;
1236
- const rowH = Math.min(0.5, (BOT - y0 - 0.2) / n);
1237
- crit.forEach((c, i) => {
1238
- const ry = y0 + 0.1 + i * rowH;
1239
- txt(slide, sc(c.name), { x: MX, y: ry, w: labelW - 0.1, h: rowH, fontSize: 10.5, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.body });
1240
- const max = t9N(c.maxScore) || 10;
1241
- const sc2 = t9N(c.score);
1242
- const w = Math.max(0.02, (sc2 / max) * plotW);
1243
- slide.addShape('rect', { x: plotX, y: ry + rowH / 2 - 0.1, w: plotW, h: 0.2, fill: { color: pal.surfaceElevated }, line: { width: 0 } });
1244
- slide.addShape('rect', { x: plotX, y: ry + rowH / 2 - 0.1, w, h: 0.2, fill: { color: series(pal)[i % 6] }, line: { width: 0 } });
1245
- txt(slide, sc2.toFixed(1) + '/' + max, { x: plotX + plotW + 0.05, y: ry, w: 1.1, h: rowH, fontSize: 9.5, color: pal.ink2, valign: 'middle', fontFace: S.fonts.mono });
1246
- });
1247
- if (p.totalScore)
1248
- txt(slide, '总分 ' + sc(p.totalScore), { x: MX, y: BOT - 0.1, w: CW, h: 0.25, fontSize: 11, color: pal.accent, bold: true, fontFace: S.fonts.mono });
1249
- footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1250
- }
1251
- // 24 takeaway(ink)
1252
- function renderTakeaway(slide, p) {
1253
- const pal = P('theme09_takeaway_v1');
1254
- frame(slide, pal, { bottom: false });
1255
- const y0 = t9Head(slide, p, pal, { size: 26 });
1256
- const pts = (p.points || []).slice(0, 4);
1257
- const n = Math.max(pts.length, 1);
1258
- const rh = (BOT - y0 - 0.1) / n;
1259
- pts.forEach((pt, i) => {
1260
- const ry = y0 + i * rh;
1261
- slide.addShape('ellipse', { x: MX, y: ry + rh / 2 - 0.25, w: 0.5, h: 0.5, fill: { color: series(pal)[i % 6] }, line: { width: 0 } });
1262
- txt(slide, sc(pt.number) || String(i + 1), { x: MX, y: ry + rh / 2 - 0.25, w: 0.5, h: 0.5, fontSize: 14, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle', fontFace: S.fonts.mono });
1263
- txt(slide, sc(pt.title), { x: MX + 0.65, y: ry + 0.05, w: CW - 0.65, h: 0.3, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
1264
- txt(slide, sc(pt.summary), { x: MX + 0.65, y: ry + 0.38, w: CW - 0.65, h: rh - 0.45, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
1265
- });
1266
- folio(slide, p, pal);
1267
- }
1268
- // 25 compare(paper)
1269
- function renderCompare(slide, p) {
1270
- const pal = P('theme09_compare_v1');
1271
- frame(slide, pal, { bottom: false });
1272
- const y0 = t9Head(slide, p, pal, { size: 26 });
1273
- const dims = (p.dimensions || []).slice(0, 6);
1274
- const colW = (CW - 0.4) / 2;
1275
- const leftX = MX;
1276
- const rightX = MX + colW + 0.4;
1277
- const headerH = 0.5;
1278
- slide.addShape('rect', { x: leftX, y: y0, w: colW, h: headerH, fill: { color: pal.accent } });
1279
- slide.addShape('rect', { x: rightX, y: y0, w: colW, h: headerH, fill: { color: pal.accent2 } });
1280
- txt(slide, p.leftTitle, { x: leftX + 0.15, y: y0, w: colW - 0.3, h: headerH, fontSize: 13, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.heading });
1281
- txt(slide, p.rightTitle, { x: rightX + 0.15, y: y0, w: colW - 0.3, h: headerH, fontSize: 13, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.heading });
1282
- const rh = Math.min(0.6, (BOT - y0 - headerH - 0.1) / Math.max(dims.length, 1));
1283
- dims.forEach((d, i) => {
1284
- const ry = y0 + headerH + 0.05 + i * rh;
1285
- const winL = String(d.winner) === 'left';
1286
- slide.addShape('rect', { x: leftX, y: ry, w: colW, h: rh - 0.05, fill: { color: winL ? pal.accent : pal.surfaceElevated }, line: { color: pal.border, width: 0.75 } });
1287
- slide.addShape('rect', { x: rightX, y: ry, w: colW, h: rh - 0.05, fill: { color: !winL ? pal.accent2 : pal.surfaceElevated }, line: { color: pal.border, width: 0.75 } });
1288
- if (i === 0) {
1289
- txt(slide, sc(d.name), { x: leftX, y: y0 + headerH + 0.05, w: colW, h: 0.22, fontSize: 8.5, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1290
- }
1291
- txt(slide, sc(d.leftValue), { x: leftX + 0.15, y: ry + 0.22, w: colW - 0.3, h: rh - 0.3, fontSize: 10.5, color: winL ? 'FFFFFF' : pal.ink, bold: winL, valign: 'middle', fontFace: S.fonts.body });
1292
- txt(slide, sc(d.rightValue), { x: rightX + 0.15, y: ry + 0.22, w: colW - 0.3, h: rh - 0.3, fontSize: 10.5, color: !winL ? 'FFFFFF' : pal.ink, bold: !winL, valign: 'middle', fontFace: S.fonts.body });
1293
- });
1294
- folio(slide, p, pal);
1295
- }
1296
- // 26 process(paper)
1297
- function renderProcess(slide, p) {
1298
- const pal = P('theme09_process_v1');
1299
- frame(slide, pal, { bottom: false });
1300
- const y0 = t9Head(slide, p, pal, { size: 26 });
1301
- const steps = (p.steps || []).slice(0, 5);
1302
- const n = Math.max(steps.length, 1);
1303
- const gap = 0.25;
1304
- const cw = (CW - gap * (n - 1)) / n;
1305
- steps.forEach((s, i) => {
1306
- const cx = MX + i * (cw + gap);
1307
- card(slide, cx, y0, cw, 2.4, pal);
1308
- t9Pill(slide, String(i + 1), cx + 0.18, y0 + 0.18, 0.4, 0.4, series(pal)[i % 6], 'FFFFFF');
1309
- txt(slide, sc(s.name), { x: cx + 0.66, y: y0 + 0.18, w: cw - 0.8, h: 0.4, fontSize: 12, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.heading });
1310
- txt(slide, sc(s.description), { x: cx + 0.18, y: y0 + 0.7, w: cw - 0.36, h: 1.5, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
1311
- if (i < n - 1)
1312
- slide.addShape('line', { x: cx + cw, y: y0 + 1.2, w: gap, h: 0, line: { color: pal.accent, width: 1.5, endArrowType: 'triangle' } });
1313
- });
1314
- folio(slide, p, pal);
1315
- }
1316
- // 27 faq(paper)
1317
- function renderFaq(slide, p) {
1318
- const pal = P('theme09_faq_v1');
1319
- frame(slide, pal, { bottom: false });
1320
- const y0 = t9Head(slide, p, pal, { size: 26 });
1321
- const faqs = (p.faqs || []).slice(0, 6);
1322
- const colN = 2;
1323
- const colW = (CW - 0.4) / 2;
1324
- const rowH = Math.min(0.85, (BOT - y0 - 0.1) / Math.ceil(faqs.length / colN));
1325
- faqs.forEach((f, i) => {
1326
- const col = i % colN;
1327
- const row = Math.floor(i / colN);
1328
- const cx = MX + col * (colW + 0.4);
1329
- const cy = y0 + row * rowH;
1330
- txt(slide, 'Q ' + sc(f.question), { x: cx, y: cy, w: colW - 0.1, h: 0.3, fontSize: 11, color: pal.accent, bold: true, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1331
- txt(slide, 'A ' + sc(f.answer), { x: cx, y: cy + 0.32, w: colW - 0.1, h: rowH - 0.36, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1332
- });
1333
- folio(slide, p, pal);
1334
- }
1335
- // ────────────────────────────────────────────────────────────────
1336
- // 通用渲染器(补齐 P1/P2 共 78 个版式,避免导出落入 "Unknown layout" 占位)
1337
- // 数据感知:能读到 chart/series/image/quote 字段即渲染真实内容,否则渲染带版式名的标注卡。
1338
- // ────────────────────────────────────────────────────────────────
1339
- function toNums(v) {
1340
- if (v == null)
1341
- return [];
1342
- if (typeof v === 'string')
1343
- return v.split(/[|,\n]/).map((s) => parseFloat(s.trim())).filter((n) => !isNaN(n));
1344
- if (Array.isArray(v))
1345
- return v.map((x) => parseFloat(Array.isArray(x) ? x[0] : x)).filter((n) => !isNaN(n));
1346
- return [];
1347
- }
1348
- function normChart(p) {
1349
- let cats = [];
1350
- if (Array.isArray(p.categories))
1351
- cats = p.categories.map(String);
1352
- else if (Array.isArray(p.labels))
1353
- cats = p.labels.map(String);
1354
- else if (Array.isArray(p.quarters))
1355
- cats = p.quarters.map(String);
1356
- else if (Array.isArray(p.periods))
1357
- cats = p.periods.map(String);
1358
- else if (Array.isArray(p.bins))
1359
- cats = p.bins.map((b) => String(b.range ?? b.label ?? b));
1360
- let s = [];
1361
- if (Array.isArray(p.series))
1362
- s = p.series.map((x) => ({ name: x.name || '', values: toNums(x.data ?? x.values ?? x.points) }));
1363
- else if (Array.isArray(p.mainSeries))
1364
- s = [{ name: p.mainName || '主序列', values: toNums(p.mainSeries) }];
1365
- if (!cats.length && s.length)
1366
- cats = s[0].values.map((_, i) => String(i + 1));
1367
- return { cats, s };
1368
- }
1369
- function renderT09ChartGeneric(slide, p, layoutId) {
1370
- const pal = P(layoutId);
1371
- frame(slide, pal);
1372
- const y0 = t9Head(slide, p, pal);
1373
- const plotX = MX, plotW = CW, plotY = y0 + 0.2, plotH = Math.max(1.6, BOT - plotY - 0.2);
1374
- const { cats, s } = normChart(p);
1375
- if (s.length && cats.length) {
1376
- const data = s.slice(0, 6).map((se, i) => ({ name: se.name || '系列 ' + (i + 1), labels: cats.slice(0, 12), values: se.values.slice(0, 12) }));
1377
- slide.addChart('bar', data, {
1378
- x: plotX, y: plotY, w: plotW, h: plotH,
1379
- barDir: 'col', barGrouping: 'clustered',
1380
- chartColors: s.slice(0, 6).map((_, i) => series(pal)[i % 6]),
1381
- showLegend: s.length > 1, legendPos: 'b', legendFontSize: 9, legendColor: pal.ink3,
1382
- catAxisLabelColor: pal.ink3, valAxisLabelColor: pal.ink3,
1383
- catAxisLabelFontSize: 9, valAxisLabelFontSize: 9, chartColorsOpacity: 95,
1384
- });
1385
- }
1386
- else {
1387
- card(slide, plotX, plotY, plotW, plotH, pal);
1388
- txt(slide, '图表版式 · ' + layoutId.replace('theme09_', '').replace('_v1', ''), {
1389
- x: plotX + 0.2, y: plotY + plotH / 2 - 0.2, w: plotW - 0.4, h: 0.4,
1390
- fontSize: 13, color: pal.ink3, align: 'center', fontFace: S.fonts.mono,
1391
- });
1392
- }
1393
- folio(slide, p, pal);
1394
- }
1395
- function renderT09MediaGeneric(slide, p, layoutId) {
1396
- const pal = P(layoutId);
1397
- frame(slide, pal);
1398
- const y0 = t9Head(slide, p, pal);
1399
- const h = Math.max(1.6, BOT - y0 - 1.15);
1400
- const src = [];
1401
- if (p.image)
1402
- src.push(p.image);
1403
- if (Array.isArray(p.images))
1404
- src.push(...p.images);
1405
- else if (Array.isArray(p.photos))
1406
- src.push(...p.photos);
1407
- const imgs = src.map(toImgUrl).filter((u) => typeof u === 'string' && u.length > 0).slice(0, 4);
1408
- if (imgs.length) {
1409
- const gap = 0.15;
1410
- const cw = (CW - gap * (imgs.length - 1)) / imgs.length;
1411
- imgs.forEach((u, i) => {
1412
- const x = MX + i * (cw + gap);
1413
- tryImage(slide, u, x, y0 + 0.2, cw, h, () => card(slide, x, y0 + 0.2, cw, h, pal));
1414
- });
1415
- }
1416
- else {
1417
- card(slide, MX, y0 + 0.2, CW, h, pal);
1418
- txt(slide, '影像版式 · ' + layoutId.replace('theme09_', '').replace('_v1', ''), {
1419
- x: MX + 0.2, y: y0 + 0.2 + h / 2 - 0.2, w: CW - 0.4, h: 0.4,
1420
- fontSize: 13, color: pal.ink3, align: 'center', fontFace: S.fonts.mono,
1421
- });
1422
- }
1423
- let iy = y0 + 0.2 + h + 0.12;
1424
- if (p.quote)
1425
- txt(slide, p.quote, { x: MX, y: iy, w: CW, h: 0.5, fontSize: 14, color: pal.accent, bold: true, fontFace: S.fonts.heading, italic: true });
1426
- const items = (p.items || p.points || []).slice(0, 5);
1427
- items.forEach((t, i) => {
1428
- const s2 = typeof t === 'string' ? t : (t.text || t.label || t.title || '');
1429
- txt(slide, '– ' + s2, { x: MX, y: iy + (p.quote ? 0.55 : 0) + i * 0.3, w: CW, h: 0.3, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body });
1430
- });
1431
- folio(slide, p, pal);
1432
- }
1433
- const CHART_LAYOUTS = [
1434
- 'theme09_market_overview_v1', 'theme09_streamgraph_v1', 'theme09_chord_v1', 'theme09_sunburst_v1',
1435
- 'theme09_ribbon_v1', 'theme09_rounds_v1', 'theme09_ranking_v1', 'theme09_bump_v1',
1436
- 'theme09_hero_number_v1', 'theme09_versus_v1', 'theme09_spiral_v1', 'theme09_funnel_v1',
1437
- 'theme09_stat_grid_v1', 'theme09_arc_v1', 'theme09_network_v1', 'theme09_area_v1',
1438
- 'theme09_mega_number_v1', 'theme09_radar_v1', 'theme09_radialbar_v1', 'theme09_honeycomb_v1',
1439
- 'theme09_tornado_v1', 'theme09_matrix_v1', 'theme09_quadrant_v1', 'theme09_bubble_v1',
1440
- 'theme09_marimekko_v1', 'theme09_meter_v1', 'theme09_parallel_v1', 'theme09_grade_v1',
1441
- 'theme09_slope_v1', 'theme09_dumbbell_v1', 'theme09_crosstab_v1', 'theme09_tier_v1',
1442
- 'theme09_ledger_v1', 'theme09_alloc_v1', 'theme09_venn_v1', 'theme09_treemap_v1',
1443
- 'theme09_icicle_v1', 'theme09_waterfall_v1', 'theme09_heatmap_v1',
1444
- ];
1445
- const MEDIA_LAYOUTS = [
1446
- 'theme09_specimen_v1', 'theme09_photo_feature_v1', 'theme09_photo_grid_v1', 'theme09_photo_quote_v1',
1447
- 'theme09_photo_duo_v1', 'theme09_photo_panorama_v1', 'theme09_photo_stage_v1', 'theme09_storyboard_v1',
1448
- 'theme09_snapshot_tape_v1', 'theme09_epigraph_v1', 'theme09_photo_bento_v1', 'theme09_timeline_photo_v1',
1449
- 'theme09_filmstrip_v1', 'theme09_mosaic_v1', 'theme09_testimonial_v1', 'theme09_team_v1',
1450
- 'theme09_photo_ring_v1', 'theme09_divider_photo_v1', 'theme09_coverstory_v1', 'theme09_quote_portrait_v1',
1451
- 'theme09_manifesto_v1', 'theme09_annotated_v1', 'theme09_diptych_v1', 'theme09_case_folio_v1',
1452
- 'theme09_split_diagonal_v1', 'theme09_typeriver_v1', 'theme09_exhibit_wall_v1', 'theme09_masonry_v1',
1453
- 'theme09_journey_v1', 'theme09_photo_cards_v1', 'theme09_zine_spread_v1', 'theme09_photo_scene_v1',
1454
- 'theme09_spotlight_v1', 'theme09_profile_v1', 'theme09_gallery_wall_v1', 'theme09_dotmatrix_v1',
1455
- ];
1456
- const bindChart = (id) => (s, p) => renderT09ChartGeneric(s, p, id);
1457
- const bindMedia = (id) => (s, p) => renderT09MediaGeneric(s, p, id);
1458
- const CHART_RENDERERS = Object.fromEntries(CHART_LAYOUTS.map((id) => [id, bindChart(id)]));
1459
- const MEDIA_RENDERERS = Object.fromEntries(MEDIA_LAYOUTS.map((id) => [id, bindMedia(id)]));
1460
- const RENDERERS = {
1461
- theme09_cover_masthead_v1: renderCoverMasthead,
1462
- theme09_cover_bleed_v1: renderCoverBleed,
1463
- theme09_cover_dossier_v1: renderCoverDossier,
1464
- theme09_cover_colorbar_v1: renderCoverColorbar,
1465
- theme09_cover_aperture_v1: renderCoverAperture,
1466
- theme09_cover_colophon_v1: renderCoverColophon,
1467
- theme09_cover_photo_v1: renderCoverPhoto,
1468
- theme09_abstract_v1: renderAbstract,
1469
- theme09_contents_v1: renderContents,
1470
- theme09_section_v1: renderSection,
1471
- theme09_section_card_v1: renderSectionCard,
1472
- theme09_closing_v1: renderClosing,
1473
- theme09_thesis_v1: renderThesis,
1474
- theme09_risk_v1: renderRisk,
1475
- theme09_outlook_v1: renderOutlook,
1476
- theme09_conclusion_v1: renderConclusion,
1477
- theme09_bracket_v1: renderBracket,
1478
- theme09_flow_v1: renderFlow,
1479
- theme09_orbit_v1: renderOrbit,
1480
- theme09_vertical_v1: renderVertical,
1481
- theme09_calendar_v1: renderCalendar,
1482
- theme09_phases_v1: renderPhases,
1483
- theme09_gauge_v1: renderGauge,
1484
- theme09_scoreboard_v1: renderScoreboard,
1485
- theme09_trend_v1: renderTrend,
1486
- theme09_histogram_v1: renderHistogram,
1487
- theme09_plans_v1: renderPlans,
1488
- theme09_stair_v1: renderStair,
1489
- theme09_stacked_v1: renderStacked,
1490
- theme09_era_v1: renderEra,
1491
- theme09_roadmap_v1: renderRoadmap,
1492
- theme09_score_v1: renderScore,
1493
- theme09_takeaway_v1: renderTakeaway,
1494
- theme09_compare_v1: renderCompare,
1495
- theme09_process_v1: renderProcess,
1496
- theme09_faq_v1: renderFaq,
1497
- // 模板版式 id 用下划线(cross_perspective / value_chain / forecast_fan),
1498
- // 与源码 hyphen 键别名对齐,避免导出落到 Unknown layout。
1499
- 'theme09_cross_perspective_v1': renderCrossPerspective,
1500
- 'theme09_value_chain_v1': renderValueChain,
1501
- 'theme09_forecast_fan_v1': renderForecastFan,
1502
- // ── P2 图表型(按 layoutId 绑定双基底,数据感知)──
1503
- ...CHART_RENDERERS,
1504
- // ── P1 图片 / 标本 / 金句型(按 layoutId 绑定双基底,数据感知)──
1505
- ...MEDIA_RENDERERS,
1506
- };
1507
- /** 将全部 theme09 渲染器注册到导出管线的按 layoutId 索引注册表。 */
1508
- export function registerTheme09Renderers(register) {
1509
- for (const [id, fn] of Object.entries(RENDERERS)) {
1510
- register(id, fn);
1511
- }
1512
- }
1513
- //# sourceMappingURL=theme09-pptx.js.map