@lemonppt/renderer 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,908 @@
1
+ // lemonPPT - AI-powered presentation generation
2
+ // Copyright (c) 2026 lemonforme
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
4
+ //
5
+ // theme11 流光科技 · 浅色扁平科技风 — PPTX 版式渲染器。
6
+ // 采用「按角色通用渲染」策略:所有 theme11 版式共享同一渲染入口,
7
+ // 按布局 ID 与 props 字段自动分发封面/章节/内容/数据/图表/影像/结束页,
8
+ // 避免导出时落到 "Unknown layout" 占位。
9
+ let S = {
10
+ appearance: 'primary',
11
+ fonts: { heading: 'Noto Sans SC', body: 'Noto Sans SC', mono: 'Space Mono' },
12
+ chartColors: ['00BCD4', '7C4DFF', '2979FF', 'FF9100', '00C853', 'FF5252'],
13
+ };
14
+ /** 每次导出开始时由 export-pptx.ts 注入 appearance / 字体 / 图表色。 */
15
+ export function configureTheme11(state) {
16
+ S = state;
17
+ }
18
+ // ────────────────────────────────────────────────────────────────
19
+ // 情绪基底(PPTX 不支持复杂弥散渐变,取情绪主色作为页面底色)
20
+ // ────────────────────────────────────────────────────────────────
21
+ const MOOD_BG = {
22
+ aurora: 'E0F7FA',
23
+ daylight: 'F8FAFC',
24
+ sunset: 'FFF8E1',
25
+ };
26
+ const PALETTE = {
27
+ primary: {
28
+ ink: '1A202C',
29
+ ink2: '5A6578',
30
+ ink3: '8B95A5',
31
+ accent: '00BCD4',
32
+ accent2: '7C4DFF',
33
+ accent3: '2979FF',
34
+ orange: 'FF9100',
35
+ red: 'FF5252',
36
+ green: '00C853',
37
+ surface: 'FFFFFF',
38
+ border: 'E2E8F0',
39
+ },
40
+ muted: {
41
+ ink: '4A5568',
42
+ ink2: '718096',
43
+ ink3: 'A0AEC0',
44
+ accent: '4DD0E1',
45
+ accent2: '9575CD',
46
+ accent3: '5C9CFF',
47
+ orange: 'FFB74D',
48
+ red: 'FF8A80',
49
+ green: '69F0AE',
50
+ surface: 'FFFFFF',
51
+ border: 'E2E8F0',
52
+ },
53
+ };
54
+ function pal() {
55
+ return PALETTE[S.appearance];
56
+ }
57
+ // ────────────────────────────────────────────────────────────────
58
+ // 小工具
59
+ // ────────────────────────────────────────────────────────────────
60
+ function applyBackground(slide, mood) {
61
+ slide.background = { color: MOOD_BG[mood] };
62
+ }
63
+ function fitText(text) {
64
+ if (text === undefined || text === null)
65
+ return '';
66
+ return String(text);
67
+ }
68
+ function parseValues(values) {
69
+ if (Array.isArray(values)) {
70
+ return values
71
+ .map((v) => (typeof v === 'number' ? v : Number.parseFloat(String(v).replace(/[, %]/g, ''))))
72
+ .filter((v) => Number.isFinite(v));
73
+ }
74
+ if (typeof values === 'string' && values.length > 0) {
75
+ return values
76
+ .split(/[,,]/)
77
+ .map((s) => Number.parseFloat(s.trim().replace(/[%\s]/g, '')))
78
+ .filter((v) => Number.isFinite(v));
79
+ }
80
+ return [];
81
+ }
82
+ function normalizeCategories(cats) {
83
+ if (!cats)
84
+ return [];
85
+ if (Array.isArray(cats)) {
86
+ return cats.map((c) => (typeof c === 'string' ? c : fitText(c.text)));
87
+ }
88
+ return [];
89
+ }
90
+ function getToneColor(tone) {
91
+ const p = pal();
92
+ switch (tone) {
93
+ case 'violet':
94
+ case 'purple':
95
+ return p.accent2;
96
+ case 'blue':
97
+ return p.accent3;
98
+ case 'orange':
99
+ return p.orange;
100
+ case 'green':
101
+ return p.green;
102
+ case 'red':
103
+ return p.red;
104
+ case 'accent':
105
+ default:
106
+ return p.accent;
107
+ }
108
+ }
109
+ function getDefaultMood(layoutId) {
110
+ const id = layoutId.toLowerCase();
111
+ if (id.includes('sunset') || id.includes('closing_cta') || id.includes('risk') || id.includes('pyramid'))
112
+ return 'sunset';
113
+ if (id.includes('aurora') || id.includes('cover') || id.includes('divider') || id.includes('statement'))
114
+ return 'aurora';
115
+ return 'daylight';
116
+ }
117
+ // ────────────────────────────────────────────────────────────────
118
+ // 基础排版元素
119
+ // ────────────────────────────────────────────────────────────────
120
+ function addEyebrow(slide, text, y = 0.85) {
121
+ if (!text)
122
+ return;
123
+ slide.addText(text, {
124
+ x: 0.6,
125
+ y,
126
+ w: 8.8,
127
+ h: 0.35,
128
+ fontSize: 12,
129
+ fontFace: S.fonts.mono,
130
+ color: pal().accent,
131
+ bold: true,
132
+ letterSpacing: 1,
133
+ charSpacing: 1,
134
+ });
135
+ }
136
+ function addTitle(slide, text, y = 1.25, opts) {
137
+ if (!text)
138
+ return;
139
+ slide.addText(text, {
140
+ x: opts?.x ?? 0.6,
141
+ y,
142
+ w: opts?.w ?? 8.8,
143
+ h: 1.0,
144
+ fontSize: opts?.fontSize ?? 36,
145
+ fontFace: S.fonts.heading,
146
+ color: pal().ink,
147
+ bold: true,
148
+ align: opts?.align ?? 'left',
149
+ });
150
+ }
151
+ function addSubtitle(slide, text, y = 2.15, opts) {
152
+ if (!text)
153
+ return;
154
+ slide.addText(text, {
155
+ x: opts?.x ?? 0.6,
156
+ y,
157
+ w: opts?.w ?? 8.8,
158
+ h: 0.7,
159
+ fontSize: 18,
160
+ fontFace: S.fonts.body,
161
+ color: pal().ink2,
162
+ align: opts?.align ?? 'left',
163
+ });
164
+ }
165
+ function addBody(slide, text, y, h = 1.2) {
166
+ if (!text)
167
+ return;
168
+ slide.addText(text, {
169
+ x: 0.6,
170
+ y,
171
+ w: 8.8,
172
+ h,
173
+ fontSize: 15,
174
+ fontFace: S.fonts.body,
175
+ color: pal().ink,
176
+ });
177
+ }
178
+ function addBulletList(slide, items, yStart, x = 0.6, w = 8.8) {
179
+ const p = pal();
180
+ let y = yStart;
181
+ const list = Array.isArray(items) ? items : [];
182
+ list.forEach((item) => {
183
+ const text = typeof item === 'string' ? item : fitText(item.text);
184
+ if (!text)
185
+ return;
186
+ slide.addText('●', { x, y, w: 0.25, h: 0.35, fontSize: 11, color: p.accent });
187
+ slide.addText(text, { x: x + 0.3, y, w: w - 0.3, h: 0.35, fontSize: 14, fontFace: S.fonts.body, color: p.ink });
188
+ y += 0.42;
189
+ });
190
+ return y;
191
+ }
192
+ function isImageSrc(url) {
193
+ return /^(https?:|data:|file:|\/|[A-Za-z]:\\)/.test(url) || /\.(png|jpg|jpeg|gif|webp|svg)(\?.*)?$/i.test(url);
194
+ }
195
+ function addImageFromProp(slide, url, x, y, w, h) {
196
+ if (!url || !isImageSrc(url))
197
+ return;
198
+ try {
199
+ slide.addImage({ path: url, x, y, w, h, sizing: { type: 'crop', w, h } });
200
+ }
201
+ catch {
202
+ slide.addText('[图片]', { x, y, w, h, fontSize: 14, color: pal().ink2, align: 'center', valign: 'middle' });
203
+ }
204
+ }
205
+ // ────────────────────────────────────────────────────────────────
206
+ // 图表渲染(抽取常见字段,复杂图表降级为文本占位)
207
+ // ────────────────────────────────────────────────────────────────
208
+ function inferChartType(layoutId) {
209
+ if (layoutId.includes('hbar'))
210
+ return 'bar';
211
+ if (layoutId.includes('line'))
212
+ return 'line';
213
+ if (layoutId.includes('area'))
214
+ return 'line';
215
+ if (layoutId.includes('pie'))
216
+ return 'pie';
217
+ if (layoutId.includes('donut'))
218
+ return 'doughnut';
219
+ if (layoutId.includes('radar'))
220
+ return 'radar';
221
+ if (layoutId.includes('scatter'))
222
+ return 'scatter';
223
+ if (layoutId.includes('bubble'))
224
+ return 'bubble';
225
+ return 'bar';
226
+ }
227
+ function renderChart(slide, props, layoutId) {
228
+ const p = props;
229
+ const categories = normalizeCategories(p.categories ?? p.labels ?? p.xAxis);
230
+ const values = parseValues(p.values ?? p.data ?? p.series);
231
+ const title = fitText(p.title);
232
+ const subtitle = fitText(p.subtitle);
233
+ // 复杂/ unsupported 图表直接降级
234
+ const unsupported = /treemap|sunburst|sankey|graph|network|parallel|funnel|gauge|candlestick|heatmap|bump|radian|waterfall|quadrant|matrix|venn|orgchart|gantt|calendar|small_multiples|swimlane/.test(layoutId);
235
+ let y = 1.35;
236
+ if (title) {
237
+ addTitle(slide, title, y);
238
+ y += 0.85;
239
+ }
240
+ if (subtitle) {
241
+ addSubtitle(slide, subtitle, y);
242
+ y += 0.55;
243
+ }
244
+ if (unsupported || categories.length === 0 || values.length === 0) {
245
+ slide.addShape('roundRect', {
246
+ x: 0.8,
247
+ y,
248
+ w: 8.4,
249
+ h: 3.6,
250
+ fill: { color: pal().surface },
251
+ line: { color: pal().border, width: 1 },
252
+ rectRadius: 0.15,
253
+ });
254
+ slide.addText('(图表数据将在导出后由编辑器渲染)', {
255
+ x: 0.8,
256
+ y,
257
+ w: 8.4,
258
+ h: 3.6,
259
+ fontSize: 16,
260
+ color: pal().ink2,
261
+ align: 'center',
262
+ valign: 'middle',
263
+ fontFace: S.fonts.body,
264
+ });
265
+ return true;
266
+ }
267
+ const type = inferChartType(layoutId);
268
+ const chartColors = type === 'pie' || type === 'doughnut'
269
+ ? categories.map((_, i) => S.chartColors[i % S.chartColors.length])
270
+ : [S.chartColors[0]];
271
+ const data = [{ name: title || '数据', labels: categories.slice(0, values.length), values }];
272
+ slide.addChart(type, data, {
273
+ x: 0.7,
274
+ y,
275
+ w: 8.6,
276
+ h: 3.6,
277
+ chartColors,
278
+ showValue: true,
279
+ dataLabelColor: pal().ink,
280
+ dataLabelFontSize: 10,
281
+ });
282
+ return true;
283
+ }
284
+ // ────────────────────────────────────────────────────────────────
285
+ // 数据表格
286
+ // ────────────────────────────────────────────────────────────────
287
+ function renderTable(slide, props) {
288
+ const columns = props.columns || props.headers || [];
289
+ const rows = props.rows || props.data || [];
290
+ if (columns.length === 0 && rows.length === 0)
291
+ return false;
292
+ const headers = columns.map((c) => (typeof c === 'string' ? c : fitText(c.title ?? c.label ?? c.text)));
293
+ const body = rows.map((row) => Array.isArray(row)
294
+ ? row.map((cell) => (typeof cell === 'string' ? cell : fitText(cell.text ?? cell.value)))
295
+ : []);
296
+ if (headers.length === 0 && body.length === 0)
297
+ return false;
298
+ const tableData = [headers, ...body].filter((r) => r.length > 0);
299
+ if (tableData.length === 0)
300
+ return false;
301
+ slide.addTable(tableData, {
302
+ x: 0.6,
303
+ y: 2.3,
304
+ w: 8.8,
305
+ h: 3.2,
306
+ fontSize: 12,
307
+ fontFace: S.fonts.body,
308
+ color: pal().ink,
309
+ border: { type: 'solid', pt: 0.5, color: pal().border },
310
+ fill: { color: pal().surface },
311
+ colW: headers.map(() => 8.8 / Math.max(headers.length, 1)),
312
+ });
313
+ return true;
314
+ }
315
+ // ────────────────────────────────────────────────────────────────
316
+ // 通用卡片/指标网格
317
+ // ────────────────────────────────────────────────────────────────
318
+ function renderItemCards(slide, items, yStart, columns = 4) {
319
+ const list = Array.isArray(items) ? items.slice(0, 6) : [];
320
+ if (list.length === 0)
321
+ return yStart;
322
+ const p = pal();
323
+ const colCount = Math.min(list.length, columns);
324
+ const gap = 0.25;
325
+ const cardW = (8.8 - gap * (colCount - 1)) / colCount;
326
+ let y = yStart;
327
+ list.forEach((item, i) => {
328
+ const row = Math.floor(i / colCount);
329
+ const col = i % colCount;
330
+ const x = 0.6 + col * (cardW + gap);
331
+ const cy = y + row * 1.45;
332
+ const value = fitText(item.value ?? item.metric ?? item.number);
333
+ const label = fitText(item.label ?? item.title ?? item.name);
334
+ const change = fitText(item.change ?? item.delta);
335
+ const tone = fitText(item.tone);
336
+ const color = getToneColor(tone || (i % 2 === 0 ? 'accent' : 'violet'));
337
+ slide.addShape('roundRect', {
338
+ x,
339
+ y: cy,
340
+ w: cardW,
341
+ h: 1.25,
342
+ fill: { color: p.surface },
343
+ line: { color: p.border, width: 1 },
344
+ rectRadius: 0.12,
345
+ });
346
+ if (value) {
347
+ slide.addText(value, { x: x + 0.12, y: cy + 0.12, w: cardW - 0.24, h: 0.45, fontSize: 24, fontFace: S.fonts.heading, bold: true, color });
348
+ }
349
+ if (change) {
350
+ slide.addText(change, { x: x + cardW - 0.9, y: cy + 0.16, w: 0.78, h: 0.35, fontSize: 11, fontFace: S.fonts.mono, color: p.ink2, align: 'right' });
351
+ }
352
+ if (label) {
353
+ slide.addText(label, { x: x + 0.12, y: cy + 0.58, w: cardW - 0.24, h: 0.45, fontSize: 12, fontFace: S.fonts.body, color: p.ink2 });
354
+ }
355
+ });
356
+ return y + Math.ceil(list.length / colCount) * 1.45 + 0.2;
357
+ }
358
+ function renderFeatureCards(slide, items, yStart, columns = 3) {
359
+ const list = Array.isArray(items) ? items.slice(0, 6) : [];
360
+ if (list.length === 0)
361
+ return yStart;
362
+ const p = pal();
363
+ const colCount = Math.min(list.length, columns);
364
+ const gap = 0.25;
365
+ const cardW = (8.8 - gap * (colCount - 1)) / colCount;
366
+ let y = yStart;
367
+ list.forEach((item, i) => {
368
+ const row = Math.floor(i / colCount);
369
+ const col = i % colCount;
370
+ const x = 0.6 + col * (cardW + gap);
371
+ const cy = y + row * 1.65;
372
+ const title = fitText(item.title ?? item.label);
373
+ const desc = fitText(item.desc ?? item.description ?? item.text);
374
+ const icon = fitText(item.icon);
375
+ const tone = fitText(item.tone);
376
+ const color = getToneColor(tone || 'accent');
377
+ slide.addShape('roundRect', {
378
+ x,
379
+ y: cy,
380
+ w: cardW,
381
+ h: 1.45,
382
+ fill: { color: p.surface },
383
+ line: { color: p.border, width: 1 },
384
+ rectRadius: 0.12,
385
+ });
386
+ if (icon) {
387
+ slide.addShape('roundRect', { x: x + 0.12, y: cy + 0.12, w: 0.38, h: 0.38, fill: { color: color }, rectRadius: 0.08 });
388
+ slide.addText(icon, { x: x + 0.12, y: cy + 0.12, w: 0.38, h: 0.38, fontSize: 14, color: 'FFFFFF', align: 'center', valign: 'middle' });
389
+ }
390
+ if (title) {
391
+ slide.addText(title, { x: x + 0.12, y: cy + 0.55, w: cardW - 0.24, h: 0.35, fontSize: 14, fontFace: S.fonts.heading, bold: true, color: p.ink });
392
+ }
393
+ if (desc) {
394
+ slide.addText(desc, { x: x + 0.12, y: cy + 0.9, w: cardW - 0.24, h: 0.45, fontSize: 10, fontFace: S.fonts.body, color: p.ink3 });
395
+ }
396
+ });
397
+ return y + Math.ceil(list.length / colCount) * 1.65 + 0.2;
398
+ }
399
+ function renderComparisonCards(slide, props) {
400
+ const p = pal();
401
+ const leftTitle = fitText(props.leftTitle ?? '方案 A');
402
+ const rightTitle = fitText(props.rightTitle ?? '方案 B');
403
+ const leftItems = (props.leftItems ?? props.leftPoints ?? []);
404
+ const rightItems = (props.rightItems ?? props.rightPoints ?? []);
405
+ slide.addShape('roundRect', { x: 0.6, y: 2.5, w: 4.1, h: 3.0, fill: { color: p.surface }, line: { color: p.border }, rectRadius: 0.12 });
406
+ slide.addShape('roundRect', { x: 5.3, y: 2.5, w: 4.1, h: 3.0, fill: { color: p.surface }, line: { color: p.border }, rectRadius: 0.12 });
407
+ slide.addText(leftTitle, { x: 0.8, y: 2.65, w: 3.7, h: 0.4, fontSize: 18, fontFace: S.fonts.heading, bold: true, color: p.accent });
408
+ slide.addText(rightTitle, { x: 5.5, y: 2.65, w: 3.7, h: 0.4, fontSize: 18, fontFace: S.fonts.heading, bold: true, color: p.accent2 });
409
+ addBulletList(slide, leftItems, 3.15, 0.85, 3.6);
410
+ addBulletList(slide, rightItems, 3.15, 5.55, 3.6);
411
+ }
412
+ // ────────────────────────────────────────────────────────────────
413
+ // 影像/画廊
414
+ // ────────────────────────────────────────────────────────────────
415
+ function renderGallery(slide, props) {
416
+ const images = (props.images ?? props.items ?? []);
417
+ if (images.length === 0 && props.image) {
418
+ addImageFromProp(slide, fitText(props.image), 0, 0, 10, 5.625);
419
+ return;
420
+ }
421
+ const cols = Math.min(images.length, 3);
422
+ const gap = 0.2;
423
+ const imgW = (8.8 - gap * (cols - 1)) / cols;
424
+ const imgH = 2.4;
425
+ images.slice(0, 6).forEach((img, i) => {
426
+ const row = Math.floor(i / cols);
427
+ const col = i % cols;
428
+ const url = typeof img === 'string' ? img : fitText(img.src ?? img.image ?? img.url);
429
+ addImageFromProp(slide, url, 0.6 + col * (imgW + gap), 2.3 + row * (imgH + gap), imgW, imgH);
430
+ });
431
+ }
432
+ function renderPhotoSplit(slide, props) {
433
+ const image = fitText(props.image);
434
+ if (image) {
435
+ addImageFromProp(slide, image, 5.0, 0, 5.0, 5.625);
436
+ }
437
+ const p = pal();
438
+ slide.addShape('rect', { x: 0, y: 0, w: 5.0, h: 5.625, fill: { color: p.surface } });
439
+ addTitle(slide, fitText(props.title), 1.8, { x: 0.6, w: 4.0, fontSize: 32 });
440
+ addSubtitle(slide, fitText(props.subtitle), 2.7, { x: 0.6, w: 4.0 });
441
+ }
442
+ function renderTeam(slide, props) {
443
+ const members = (props.members ?? props.items ?? []);
444
+ renderMemberCards(slide, members, 2.4);
445
+ }
446
+ function renderMemberCards(slide, members, yStart) {
447
+ const list = Array.isArray(members) ? members.slice(0, 6) : [];
448
+ if (list.length === 0)
449
+ return yStart;
450
+ const p = pal();
451
+ const cols = Math.min(list.length, 4);
452
+ const gap = 0.25;
453
+ const cardW = (8.8 - gap * (cols - 1)) / cols;
454
+ let y = yStart;
455
+ list.forEach((m, i) => {
456
+ const row = Math.floor(i / cols);
457
+ const col = i % cols;
458
+ const x = 0.6 + col * (cardW + gap);
459
+ const cy = y + row * 1.75;
460
+ const name = fitText(m.name ?? m.title);
461
+ const role = fitText(m.role ?? m.label);
462
+ const avatar = fitText(m.avatar ?? m.image);
463
+ slide.addShape('roundRect', { x, y: cy, w: cardW, h: 1.55, fill: { color: p.surface }, line: { color: p.border }, rectRadius: 0.12 });
464
+ if (avatar) {
465
+ addImageFromProp(slide, avatar, x + cardW / 2 - 0.32, cy + 0.12, 0.64, 0.64);
466
+ }
467
+ else {
468
+ slide.addShape('ellipse', { x: x + cardW / 2 - 0.32, y: cy + 0.12, w: 0.64, h: 0.64, fill: { color: p.border } });
469
+ }
470
+ if (name)
471
+ slide.addText(name, { x, y: cy + 0.82, w: cardW, h: 0.35, fontSize: 13, fontFace: S.fonts.heading, bold: true, color: p.ink, align: 'center' });
472
+ if (role)
473
+ slide.addText(role, { x, y: cy + 1.12, w: cardW, h: 0.3, fontSize: 10, fontFace: S.fonts.body, color: p.ink2, align: 'center' });
474
+ });
475
+ return y + Math.ceil(list.length / cols) * 1.75 + 0.2;
476
+ }
477
+ // ────────────────────────────────────────────────────────────────
478
+ // 流程/时间线/步骤
479
+ // ────────────────────────────────────────────────────────────────
480
+ function renderTimeline(slide, items, yStart) {
481
+ const list = Array.isArray(items) ? items.slice(0, 6) : [];
482
+ if (list.length === 0)
483
+ return yStart;
484
+ const p = pal();
485
+ const gap = 8.8 / list.length;
486
+ let y = yStart;
487
+ list.forEach((item, i) => {
488
+ const x = 0.6 + i * gap + gap / 2 - 0.2;
489
+ const title = fitText(item.label ?? item.title ?? item.phase);
490
+ const desc = fitText(item.desc ?? item.description);
491
+ const number = fitText(item.number ?? String(i + 1));
492
+ slide.addShape('ellipse', { x, y, w: 0.4, h: 0.4, fill: { color: p.accent } });
493
+ slide.addText(number, { x, y, w: 0.4, h: 0.4, fontSize: 11, color: 'FFFFFF', align: 'center', valign: 'middle', bold: true });
494
+ if (i < list.length - 1) {
495
+ slide.addShape('line', { x1: x + 0.4, y1: y + 0.2, x2: x + gap - 0.4, y2: y + 0.2, line: { color: p.border, width: 2 } });
496
+ }
497
+ if (title)
498
+ slide.addText(title, { x: x - 0.8, y: y + 0.55, w: 2.0, h: 0.35, fontSize: 12, fontFace: S.fonts.heading, bold: true, color: p.ink, align: 'center' });
499
+ if (desc)
500
+ slide.addText(desc, { x: x - 0.8, y: y + 0.95, w: 2.0, h: 0.7, fontSize: 10, fontFace: S.fonts.body, color: p.ink2, align: 'center' });
501
+ });
502
+ return y + 1.8;
503
+ }
504
+ function renderSteps(slide, items, yStart) {
505
+ const list = Array.isArray(items) ? items.slice(0, 5) : [];
506
+ if (list.length === 0)
507
+ return yStart;
508
+ const p = pal();
509
+ let y = yStart;
510
+ list.forEach((item, i) => {
511
+ const title = fitText(item.title ?? item.label);
512
+ const desc = fitText(item.desc ?? item.description);
513
+ slide.addShape('roundRect', { x: 0.6, y, w: 0.4, h: 0.4, fill: { color: p.accent }, rectRadius: 0.08 });
514
+ slide.addText(String(i + 1), { x: 0.6, y, w: 0.4, h: 0.4, fontSize: 14, color: 'FFFFFF', align: 'center', valign: 'middle', bold: true });
515
+ if (title)
516
+ slide.addText(title, { x: 1.15, y, w: 8.2, h: 0.35, fontSize: 15, fontFace: S.fonts.heading, bold: true, color: p.ink });
517
+ if (desc)
518
+ slide.addText(desc, { x: 1.15, y: y + 0.35, w: 8.2, h: 0.35, fontSize: 12, fontFace: S.fonts.body, color: p.ink2 });
519
+ y += 0.9;
520
+ });
521
+ return y;
522
+ }
523
+ // ────────────────────────────────────────────────────────────────
524
+ // 定价/方案
525
+ // ────────────────────────────────────────────────────────────────
526
+ function renderPricing(slide, props) {
527
+ const plans = (props.plans ?? props.items ?? props.cards ?? []);
528
+ const list = Array.isArray(plans) ? plans.slice(0, 3) : [];
529
+ if (list.length === 0)
530
+ return;
531
+ const p = pal();
532
+ const cols = list.length;
533
+ const gap = 0.25;
534
+ const cardW = (8.8 - gap * (cols - 1)) / cols;
535
+ const y = 2.4;
536
+ list.forEach((plan, i) => {
537
+ const x = 0.6 + i * (cardW + gap);
538
+ const name = fitText(plan.name ?? plan.title);
539
+ const price = fitText(plan.price ?? plan.value);
540
+ const features = (plan.features ?? plan.items ?? []);
541
+ const active = !!plan.active || !!plan.highlight;
542
+ slide.addShape('roundRect', {
543
+ x,
544
+ y,
545
+ w: cardW,
546
+ h: 3.2,
547
+ fill: { color: active ? p.accent : p.surface },
548
+ line: { color: active ? p.accent : p.border, width: 1.5 },
549
+ rectRadius: 0.15,
550
+ });
551
+ const textColor = active ? 'FFFFFF' : p.ink;
552
+ if (name)
553
+ slide.addText(name, { x, y: y + 0.2, w: cardW, h: 0.4, fontSize: 16, fontFace: S.fonts.heading, bold: true, color: textColor, align: 'center' });
554
+ if (price)
555
+ slide.addText(price, { x, y: y + 0.65, w: cardW, h: 0.5, fontSize: 28, fontFace: S.fonts.heading, bold: true, color: active ? 'FFFFFF' : p.accent, align: 'center' });
556
+ features.slice(0, 4).forEach((f, idx) => {
557
+ const text = typeof f === 'string' ? f : fitText(f.text);
558
+ slide.addText(text, { x: x + 0.15, y: y + 1.25 + idx * 0.35, w: cardW - 0.3, h: 0.32, fontSize: 11, fontFace: S.fonts.body, color: textColor, align: 'center' });
559
+ });
560
+ });
561
+ }
562
+ // ────────────────────────────────────────────────────────────────
563
+ // FAQ / 检查清单 / 引用
564
+ // ────────────────────────────────────────────────────────────────
565
+ function renderFaq(slide, props) {
566
+ const items = (props.items ?? props.qa ?? []);
567
+ const p = pal();
568
+ let y = 2.3;
569
+ items.slice(0, 4).forEach((item) => {
570
+ const q = fitText(item.question ?? item.q ?? item.title);
571
+ const a = fitText(item.answer ?? item.a ?? item.desc);
572
+ slide.addShape('roundRect', { x: 0.6, y, w: 8.8, h: 0.7, fill: { color: p.surface }, line: { color: p.border }, rectRadius: 0.1 });
573
+ slide.addText(q, { x: 0.8, y: y + 0.1, w: 8.4, h: 0.28, fontSize: 13, fontFace: S.fonts.heading, bold: true, color: p.ink });
574
+ slide.addText(a, { x: 0.8, y: y + 0.36, w: 8.4, h: 0.28, fontSize: 11, fontFace: S.fonts.body, color: p.ink2 });
575
+ y += 0.82;
576
+ });
577
+ }
578
+ function renderQuote(slide, props) {
579
+ const quote = fitText(props.quote ?? props.text ?? props.statement);
580
+ const author = fitText(props.author ?? props.name ?? props.source);
581
+ const p = pal();
582
+ if (quote) {
583
+ slide.addText('“', { x: 0.6, y: 1.9, w: 0.6, h: 0.6, fontSize: 48, fontFace: S.fonts.heading, color: p.accent });
584
+ slide.addText(quote, { x: 1.2, y: 2.1, w: 7.8, h: 1.6, fontSize: 24, fontFace: S.fonts.heading, color: p.ink, italic: true });
585
+ }
586
+ if (author) {
587
+ slide.addText(`— ${author}`, { x: 1.2, y: 3.8, w: 7.8, h: 0.4, fontSize: 14, fontFace: S.fonts.body, color: p.ink2 });
588
+ }
589
+ }
590
+ // ────────────────────────────────────────────────────────────────
591
+ // 主入口:按角色分发
592
+ // ────────────────────────────────────────────────────────────────
593
+ export function renderTheme11Slide(slide, props, layoutId) {
594
+ const p = props;
595
+ const mood = p.mood || getDefaultMood(layoutId);
596
+ applyBackground(slide, mood);
597
+ const id = layoutId.toLowerCase();
598
+ const title = fitText(p.title);
599
+ const subtitle = fitText(p.subtitle);
600
+ const eyebrow = fitText(p.eyebrow ?? p.tagline ?? p.kicker ?? p.label);
601
+ // 封面
602
+ if (id.includes('cover')) {
603
+ if (p.image) {
604
+ addImageFromProp(slide, fitText(p.image), 0, 0, 10, 5.625);
605
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: 'FFFFFF', transparency: 70 } });
606
+ }
607
+ if (eyebrow)
608
+ addEyebrow(slide, eyebrow, 1.3);
609
+ addTitle(slide, title, 1.8, { align: 'center', x: 0.6, w: 8.8, fontSize: 44 });
610
+ addSubtitle(slide, subtitle, 2.9, { align: 'center', x: 1, w: 8 });
611
+ if (p.date) {
612
+ slide.addText(fitText(p.date), { x: 0.6, y: 4.9, w: 8.8, h: 0.3, fontSize: 12, fontFace: S.fonts.mono, color: pal().ink3, align: 'center' });
613
+ }
614
+ return;
615
+ }
616
+ // 章节 / 分隔 / 金句 / 过渡
617
+ if (id.includes('chapter') || id.includes('divider') || id.includes('statement') || id.includes('transition')) {
618
+ if (eyebrow)
619
+ addEyebrow(slide, eyebrow, 1.8);
620
+ if (p.number || p.keyword) {
621
+ slide.addText(fitText(p.number ?? p.keyword), {
622
+ x: 0.6,
623
+ y: 2.0,
624
+ w: 8.8,
625
+ h: 1.0,
626
+ fontSize: 72,
627
+ fontFace: S.fonts.heading,
628
+ bold: true,
629
+ color: pal().accent,
630
+ align: 'center',
631
+ });
632
+ addTitle(slide, title, 3.1, { align: 'center', x: 0.6, w: 8.8, fontSize: 32 });
633
+ }
634
+ else {
635
+ addTitle(slide, title, 2.4, { align: 'center', x: 0.6, w: 8.8, fontSize: 40 });
636
+ }
637
+ addSubtitle(slide, subtitle, 3.4, { align: 'center', x: 1, w: 8 });
638
+ if (p.image) {
639
+ addImageFromProp(slide, fitText(p.image), 2, 3.8, 6, 1.4);
640
+ }
641
+ return;
642
+ }
643
+ // 结束页
644
+ if (id.includes('closing') || id.includes('back_cover')) {
645
+ addTitle(slide, title, 1.8, { align: 'center', x: 0.6, w: 8.8, fontSize: 42 });
646
+ addSubtitle(slide, subtitle, 2.8, { align: 'center', x: 1, w: 8 });
647
+ if (p.cta) {
648
+ slide.addShape('roundRect', { x: 3.5, y: 3.6, w: 3, h: 0.6, fill: { color: pal().accent }, rectRadius: 0.3 });
649
+ slide.addText(fitText(p.cta), { x: 3.5, y: 3.6, w: 3, h: 0.6, fontSize: 16, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle' });
650
+ }
651
+ const contacts = [p.contact, p.email, p.link, p.phone, p.website].filter(Boolean).map(fitText);
652
+ let cy = 4.5;
653
+ contacts.forEach((c) => {
654
+ slide.addText(c, { x: 0.6, y: cy, w: 8.8, h: 0.3, fontSize: 13, fontFace: S.fonts.mono, color: pal().accent2, align: 'center' });
655
+ cy += 0.38;
656
+ });
657
+ return;
658
+ }
659
+ // 通用头部
660
+ let cursorY = 1.25;
661
+ if (eyebrow) {
662
+ addEyebrow(slide, eyebrow, cursorY);
663
+ cursorY += 0.45;
664
+ }
665
+ if (title) {
666
+ addTitle(slide, title, cursorY);
667
+ cursorY += 0.9;
668
+ }
669
+ if (subtitle) {
670
+ addSubtitle(slide, subtitle, cursorY);
671
+ cursorY += 0.6;
672
+ }
673
+ // 图表
674
+ if (id.includes('chart') || id.includes('quadrant') || id.includes('matrix')) {
675
+ renderChart(slide, p, layoutId);
676
+ return;
677
+ }
678
+ // 表格
679
+ if (id.includes('table')) {
680
+ renderTable(slide, p);
681
+ return;
682
+ }
683
+ // 影像与分屏
684
+ if (id.includes('photo_split')) {
685
+ renderPhotoSplit(slide, p);
686
+ return;
687
+ }
688
+ if (id.includes('photo_feature') && p.image) {
689
+ addImageFromProp(slide, fitText(p.image), 0, 0, 10, 5.625);
690
+ slide.addShape('rect', { x: 0, y: 4.0, w: 10, h: 1.625, fill: { color: 'FFFFFF', transparency: 80 } });
691
+ addTitle(slide, title, 4.15, { x: 0.6, w: 8.8, fontSize: 28 });
692
+ return;
693
+ }
694
+ // 画廊
695
+ if (id.includes('gallery') || id.includes('showcase')) {
696
+ renderGallery(slide, p);
697
+ return;
698
+ }
699
+ // 团队
700
+ if (id.includes('team')) {
701
+ renderTeam(slide, p);
702
+ return;
703
+ }
704
+ // 引用/证言
705
+ if (id.includes('quote') || id.includes('testimonial')) {
706
+ if (id.includes('portrait') && p.image) {
707
+ addImageFromProp(slide, fitText(p.image), 0.6, 2.2, 2.5, 2.5);
708
+ renderQuote(slide, { ...p, quote: p.quote ?? p.text });
709
+ }
710
+ else {
711
+ renderQuote(slide, p);
712
+ }
713
+ return;
714
+ }
715
+ // 定价/方案
716
+ if (id.includes('pricing') || id.includes('plans')) {
717
+ renderPricing(slide, p);
718
+ return;
719
+ }
720
+ // 对比
721
+ if (id.includes('comparison') || id.includes('swot') || id.includes('pest')) {
722
+ renderComparisonCards(slide, p);
723
+ return;
724
+ }
725
+ // FAQ
726
+ if (id.includes('faq')) {
727
+ renderFaq(slide, p);
728
+ return;
729
+ }
730
+ // 检查清单
731
+ if (id.includes('checklist')) {
732
+ const items = (p.items ?? p.checklist ?? []);
733
+ addBulletList(slide, items, cursorY);
734
+ return;
735
+ }
736
+ // 时间线/路线图/流程/步骤
737
+ if (id.includes('timeline') || id.includes('roadmap')) {
738
+ renderTimeline(slide, (p.items ?? p.chapters ?? p.milestones ?? []), cursorY + 0.2);
739
+ return;
740
+ }
741
+ if (id.includes('process') || id.includes('steps') || id.includes('cycle')) {
742
+ renderSteps(slide, (p.items ?? p.steps ?? p.phases ?? []), cursorY + 0.2);
743
+ return;
744
+ }
745
+ // 指标/数据
746
+ if (id.includes('metric') || id.includes('stats') || id.includes('kpi') || id.includes('scorecard') || id.includes('index_board') || id.includes('progress') || id.includes('ranking') || id.includes('hero_number') || id.includes('big_number')) {
747
+ if (p.value || p.number || p.metric) {
748
+ slide.addText(fitText(p.value ?? p.number ?? p.metric), {
749
+ x: 0.6,
750
+ y: cursorY,
751
+ w: 8.8,
752
+ h: 1.0,
753
+ fontSize: 56,
754
+ fontFace: S.fonts.heading,
755
+ bold: true,
756
+ color: pal().accent,
757
+ });
758
+ cursorY += 1.0;
759
+ }
760
+ renderItemCards(slide, (p.items ?? p.metrics ?? p.data ?? []), cursorY + 0.2, 4);
761
+ return;
762
+ }
763
+ // 特性卡片
764
+ if (id.includes('feature') || id.includes('card') || id.includes('principles') || id.includes('glossary') || id.includes('case') || id.includes('editorial')) {
765
+ renderFeatureCards(slide, (p.items ?? p.cards ?? p.features ?? p.terms ?? p.points ?? []), cursorY + 0.2, 3);
766
+ return;
767
+ }
768
+ // 目录
769
+ if (id.includes('contents') || id.includes('toc')) {
770
+ const items = (p.items ?? p.sections ?? p.chapters ?? []);
771
+ addBulletList(slide, items, cursorY);
772
+ return;
773
+ }
774
+ // 通用内容兜底:body / bullets / image
775
+ if (p.body) {
776
+ addBody(slide, fitText(p.body), cursorY);
777
+ }
778
+ else if (p.points || p.items || p.bullets) {
779
+ addBulletList(slide, (p.points ?? p.items ?? p.bullets), cursorY);
780
+ }
781
+ else if (p.image) {
782
+ addImageFromProp(slide, fitText(p.image), 2.5, cursorY, 5, 2.8);
783
+ }
784
+ else if (!title && !subtitle) {
785
+ slide.addText(`Theme 11 · ${layoutId}`, { x: 0.6, y: 2.5, w: 8.8, h: 0.5, fontSize: 14, color: pal().ink2, align: 'center' });
786
+ }
787
+ }
788
+ // ────────────────────────────────────────────────────────────────
789
+ // 注册全部 theme11 版式
790
+ // ────────────────────────────────────────────────────────────────
791
+ const THEME11_LAYOUTS = [
792
+ 'theme11_back_cover_v1',
793
+ 'theme11_bento_v1',
794
+ 'theme11_big_number_v1',
795
+ 'theme11_calendar_v1',
796
+ 'theme11_case_study_v1',
797
+ 'theme11_case_v1',
798
+ 'theme11_chapter_cards_v1',
799
+ 'theme11_chapter_hero_v1',
800
+ 'theme11_chapter_numbered_v1',
801
+ 'theme11_chapter_split_v1',
802
+ 'theme11_chapter_timeline_v1',
803
+ 'theme11_chapter_v1',
804
+ 'theme11_chart_area_v1',
805
+ 'theme11_chart_bar_v1',
806
+ 'theme11_chart_bubble_v1',
807
+ 'theme11_chart_bump_v1',
808
+ 'theme11_chart_candlestick_v1',
809
+ 'theme11_chart_donut_v1',
810
+ 'theme11_chart_funnel_v1',
811
+ 'theme11_chart_gauge_v1',
812
+ 'theme11_chart_graph_v1',
813
+ 'theme11_chart_grouped_v1',
814
+ 'theme11_chart_hbar_v1',
815
+ 'theme11_chart_heatmap_v1',
816
+ 'theme11_chart_line_v1',
817
+ 'theme11_chart_parallel_v1',
818
+ 'theme11_chart_pie_v1',
819
+ 'theme11_chart_radar_v1',
820
+ 'theme11_chart_radian_v1',
821
+ 'theme11_chart_sankey_v1',
822
+ 'theme11_chart_scatter_v1',
823
+ 'theme11_chart_stack_v1',
824
+ 'theme11_chart_sunburst_v1',
825
+ 'theme11_chart_treemap_v1',
826
+ 'theme11_chart_waterfall_v1',
827
+ 'theme11_checklist_v1',
828
+ 'theme11_closing_contact_v1',
829
+ 'theme11_closing_cta_v1',
830
+ 'theme11_closing_minimal_v1',
831
+ 'theme11_closing_quote_v1',
832
+ 'theme11_closing_social_v1',
833
+ 'theme11_closing_split_v1',
834
+ 'theme11_closing_v1',
835
+ 'theme11_comparison_cards_v1',
836
+ 'theme11_comparison_v1',
837
+ 'theme11_components_showcase_v1',
838
+ 'theme11_contents_v1',
839
+ 'theme11_cover_aurora_v1',
840
+ 'theme11_cover_daylight_v1',
841
+ 'theme11_cover_product_v1',
842
+ 'theme11_cover_split_v1',
843
+ 'theme11_cover_sunset_v1',
844
+ 'theme11_cycle_v1',
845
+ 'theme11_divider_v1',
846
+ 'theme11_editorial_v1',
847
+ 'theme11_faq_v1',
848
+ 'theme11_feature_cards_v1',
849
+ 'theme11_feature_grid_v1',
850
+ 'theme11_feature_v1',
851
+ 'theme11_gallery_v1',
852
+ 'theme11_gallery_wall_v1',
853
+ 'theme11_gantt_v1',
854
+ 'theme11_glossary_v1',
855
+ 'theme11_hero_number_v1',
856
+ 'theme11_index_board_v1',
857
+ 'theme11_kpi_strip_v1',
858
+ 'theme11_matrix_v1',
859
+ 'theme11_metric_big_v1',
860
+ 'theme11_metrics_v1',
861
+ 'theme11_network_v1',
862
+ 'theme11_orgchart_v1',
863
+ 'theme11_partners_v1',
864
+ 'theme11_pest_v1',
865
+ 'theme11_photo_feature_v1',
866
+ 'theme11_photo_split_v1',
867
+ 'theme11_plans_v1',
868
+ 'theme11_pricing_v1',
869
+ 'theme11_principles_v1',
870
+ 'theme11_process_v1',
871
+ 'theme11_progress_v1',
872
+ 'theme11_pyramid_v1',
873
+ 'theme11_quadrant_v1',
874
+ 'theme11_quote_portrait_v1',
875
+ 'theme11_quote_v1',
876
+ 'theme11_ranking_v1',
877
+ 'theme11_risk_v1',
878
+ 'theme11_roadmap_v1',
879
+ 'theme11_scorecard_v1',
880
+ 'theme11_showcase_v1',
881
+ 'theme11_small_multiples_v1',
882
+ 'theme11_stat_strip_v1',
883
+ 'theme11_statement_v1',
884
+ 'theme11_steps_v1',
885
+ 'theme11_summary_v1',
886
+ 'theme11_swimlane_v1',
887
+ 'theme11_swot_v1',
888
+ 'theme11_table_data_v1',
889
+ 'theme11_table_of_contents_v1',
890
+ 'theme11_table_v1',
891
+ 'theme11_team_v1',
892
+ 'theme11_testimonial_v1',
893
+ 'theme11_timeline_v1',
894
+ 'theme11_transition_image_v1',
895
+ 'theme11_transition_minimal_v1',
896
+ 'theme11_trend_v1',
897
+ 'theme11_venn_v1',
898
+ ];
899
+ const THEME11_RENDERERS = Object.fromEntries(THEME11_LAYOUTS.map((id) => [id, (slide, props) => renderTheme11Slide(slide, props, id)]));
900
+ const RENDERERS = {
901
+ ...THEME11_RENDERERS,
902
+ };
903
+ export function registerTheme11Renderers(register) {
904
+ for (const [id, fn] of Object.entries(RENDERERS)) {
905
+ register(id, fn);
906
+ }
907
+ }
908
+ //# sourceMappingURL=theme11-pptx.js.map