html2pptx-local-mcp 1.1.17

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,584 @@
1
+ // src/animation-renderers.js
2
+ //
3
+ // Per-preset OOXML timing renderers.
4
+ //
5
+ // This replaces the previous "opacity 0→1 for every preset" approach with
6
+ // proper preset-specific animation XML. Each renderer returns the
7
+ // <p:childTnLst> contents for a given entrance / emphasis / exit effect.
8
+ //
9
+ // Three families:
10
+ //
11
+ // 1. animEffect — a high-level PowerPoint filter ("fade", "wipe(right)",
12
+ // "blinds(vertical)", etc.). PowerPoint renders them natively and
13
+ // correctly — this covers ~70% of the good-looking presets.
14
+ //
15
+ // 2. anim(attr) — drive a specific attribute (ppt_x, ppt_y, ppt_w,
16
+ // ppt_h, style.rotation, style.opacity) over time. Used for motion-
17
+ // based effects like flyin (ppt_x), zoom (ppt_w), spin (rotation).
18
+ //
19
+ // 3. combinations — entrance effects often need BOTH (visibility set +
20
+ // motion anim). Renderers compose them.
21
+ //
22
+ // All renderers take a uniform context object:
23
+ // { baseId, durMs, delayMs, spid, direction, easing }
24
+ // and return an XML string that fits inside <p:childTnLst>…</p:childTnLst>.
25
+ //
26
+ // The outer <p:par><p:cTn presetID=... presetClass=... presetSubtype=...>
27
+ // wrapper is built by the caller in animation-injector.js.
28
+
29
+ // ----------------------------------------------------------------------
30
+ // Ease curves
31
+ // ----------------------------------------------------------------------
32
+ //
33
+ // <p:anim> supports:
34
+ // calcmode="lin" — linear
35
+ // calcmode="discrete" — stepped (used only for set-style effects)
36
+ // calcmode="spline" — with explicit keySplines ("cpX1 cpY1 cpX2 cpY2")
37
+ //
38
+ // We use ease-out by default (natural deceleration) which reads as
39
+ // "premium / Apple-like" rather than mechanical.
40
+ const EASE_OUT_SPLINE = '0,0 0.25,1';
41
+ const EASE_IN_OUT_SPLINE = '0.42,0 0.58,1';
42
+
43
+ // ----------------------------------------------------------------------
44
+ // Helpers
45
+ // ----------------------------------------------------------------------
46
+
47
+ function escapeXmlAttr(s) {
48
+ return String(s)
49
+ .replace(/&/g, '&amp;')
50
+ .replace(/"/g, '&quot;')
51
+ .replace(/</g, '&lt;')
52
+ .replace(/>/g, '&gt;');
53
+ }
54
+
55
+ function setVisible(spid, id) {
56
+ return `
57
+ <p:set>
58
+ <p:cBhvr>
59
+ <p:cTn id="${id}" dur="1" fill="hold">
60
+ <p:stCondLst><p:cond delay="0"/></p:stCondLst>
61
+ </p:cTn>
62
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
63
+ <p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>
64
+ </p:cBhvr>
65
+ <p:to><p:strVal val="visible"/></p:to>
66
+ </p:set>`;
67
+ }
68
+
69
+ function setHidden(spid, id) {
70
+ return `
71
+ <p:set>
72
+ <p:cBhvr>
73
+ <p:cTn id="${id}" dur="1" fill="hold">
74
+ <p:stCondLst><p:cond delay="0"/></p:stCondLst>
75
+ </p:cTn>
76
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
77
+ <p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>
78
+ </p:cBhvr>
79
+ <p:to><p:strVal val="hidden"/></p:to>
80
+ </p:set>`;
81
+ }
82
+
83
+ // Emit a <p:animEffect> block with the given filter.
84
+ // Transition is "in" for entrance, "out" for exit, "none" for emphasis.
85
+ function animEffect({ id, durMs, spid, filter, transition = 'in' }) {
86
+ return `
87
+ <p:animEffect transition="${transition}" filter="${escapeXmlAttr(filter)}">
88
+ <p:cBhvr>
89
+ <p:cTn id="${id}" dur="${durMs}"/>
90
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
91
+ </p:cBhvr>
92
+ </p:animEffect>`;
93
+ }
94
+
95
+ // Emit a <p:anim> block that drives a single attribute (e.g. ppt_x) from
96
+ // `fromVal` to `toVal` over `durMs`.
97
+ function attrAnim({ id, durMs, spid, attrName, fromVal, toVal, easing = 'spline', spline = EASE_OUT_SPLINE, valueType = 'num' }) {
98
+ const calcmode = easing === 'lin' ? 'lin' : 'spline';
99
+ const splineAttr = calcmode === 'spline' ? ` keySpline="${spline}"` : '';
100
+ return `
101
+ <p:anim calcmode="${calcmode}"${splineAttr} valueType="${valueType}">
102
+ <p:cBhvr additive="base">
103
+ <p:cTn id="${id}" dur="${durMs}" fill="hold"/>
104
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
105
+ <p:attrNameLst><p:attrName>${attrName}</p:attrName></p:attrNameLst>
106
+ </p:cBhvr>
107
+ <p:tavLst>
108
+ <p:tav tm="0"><p:val><p:strVal val="${escapeXmlAttr(fromVal)}"/></p:val></p:tav>
109
+ <p:tav tm="100000"><p:val><p:strVal val="${escapeXmlAttr(toVal)}"/></p:val></p:tav>
110
+ </p:tavLst>
111
+ </p:anim>`;
112
+ }
113
+
114
+ // Fade-based opacity animation (used as emphasis helper)
115
+ function opacityAnim({ id, durMs, spid, from, to }) {
116
+ return attrAnim({ id, durMs, spid, attrName: 'style.opacity', fromVal: String(from), toVal: String(to) });
117
+ }
118
+
119
+ // ----------------------------------------------------------------------
120
+ // Direction helpers
121
+ // ----------------------------------------------------------------------
122
+
123
+ // For flyin/flyout, map cardinal direction → from/to values on ppt_x or ppt_y.
124
+ // - "left" means "enter from left edge of slide" → from ppt_x = negative width
125
+ // - "right" means "enter from right edge" → from ppt_x = slide_w
126
+ // - "up" means "enter from top" → from ppt_y = negative height
127
+ // - "down" means "enter from bottom" → from ppt_y = slide_h
128
+ //
129
+ // We use PowerPoint formula syntax: #ppt_x references the shape's current X.
130
+ // For "from offscreen left": x = "#ppt_x - #ppt_w"
131
+ function flyFromForEntrance(direction) {
132
+ switch (direction) {
133
+ case 'right': return { attr: 'ppt_x', from: '#ppt_x + #ppt_w', to: '#ppt_x' };
134
+ case 'up': return { attr: 'ppt_y', from: '#ppt_y - #ppt_h', to: '#ppt_y' };
135
+ case 'down': return { attr: 'ppt_y', from: '#ppt_y + #ppt_h', to: '#ppt_y' };
136
+ case 'topleft': return null; // covered by two anims — caller handles
137
+ case 'topright': return null;
138
+ case 'bottomleft': return null;
139
+ case 'bottomright': return null;
140
+ case 'left':
141
+ default: return { attr: 'ppt_x', from: '#ppt_x - #ppt_w', to: '#ppt_x' };
142
+ }
143
+ }
144
+ function flyToForExit(direction) {
145
+ switch (direction) {
146
+ case 'right': return { attr: 'ppt_x', from: '#ppt_x', to: '#ppt_x + #ppt_w' };
147
+ case 'up': return { attr: 'ppt_y', from: '#ppt_y', to: '#ppt_y - #ppt_h' };
148
+ case 'down': return { attr: 'ppt_y', from: '#ppt_y', to: '#ppt_y + #ppt_h' };
149
+ case 'left':
150
+ default: return { attr: 'ppt_x', from: '#ppt_x', to: '#ppt_x - #ppt_w' };
151
+ }
152
+ }
153
+
154
+ // For animEffect-based wipes, map our cardinal direction → PowerPoint filter
155
+ function wipeFilter(direction) {
156
+ switch (direction) {
157
+ case 'right': return 'wipe(right)';
158
+ case 'up': return 'wipe(up)';
159
+ case 'down': return 'wipe(down)';
160
+ case 'left':
161
+ default: return 'wipe(left)';
162
+ }
163
+ }
164
+
165
+ // ======================================================================
166
+ // ENTRANCE RENDERERS
167
+ // ======================================================================
168
+
169
+ function renderFadein(ctx) {
170
+ const { baseId, durMs, spid } = ctx;
171
+ return [
172
+ setVisible(spid, baseId + 1),
173
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
174
+ ].join('');
175
+ }
176
+
177
+ function renderAppear(ctx) {
178
+ const { baseId, spid } = ctx;
179
+ return setVisible(spid, baseId + 1);
180
+ }
181
+
182
+ function renderFlyin(ctx) {
183
+ const { baseId, durMs, spid, direction = 'left' } = ctx;
184
+ const fly = flyFromForEntrance(direction);
185
+ // Diagonal flyin uses both X and Y
186
+ if (!fly) {
187
+ const xDir = direction.includes('right') ? 'right' : 'left';
188
+ const yDir = direction.includes('top') ? 'up' : 'down';
189
+ const flyX = flyFromForEntrance(xDir);
190
+ const flyY = flyFromForEntrance(yDir);
191
+ return [
192
+ setVisible(spid, baseId + 1),
193
+ attrAnim({ id: baseId + 2, durMs, spid, attrName: flyX.attr, fromVal: flyX.from, toVal: flyX.to }),
194
+ attrAnim({ id: baseId + 3, durMs, spid, attrName: flyY.attr, fromVal: flyY.from, toVal: flyY.to }),
195
+ ].join('');
196
+ }
197
+ return [
198
+ setVisible(spid, baseId + 1),
199
+ attrAnim({ id: baseId + 2, durMs, spid, attrName: fly.attr, fromVal: fly.from, toVal: fly.to }),
200
+ ].join('');
201
+ }
202
+
203
+ function renderFloatin(ctx) {
204
+ // Float in = fade + slight position drift (shorter distance than flyin).
205
+ const { baseId, durMs, spid, direction = 'up' } = ctx;
206
+ const drift = direction === 'down' ? '#ppt_y - #ppt_h/3' : '#ppt_y + #ppt_h/3';
207
+ const attrName = 'ppt_y';
208
+ return [
209
+ setVisible(spid, baseId + 1),
210
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
211
+ attrAnim({ id: baseId + 3, durMs, spid, attrName, fromVal: drift, toVal: '#ppt_y' }),
212
+ ].join('');
213
+ }
214
+
215
+ function renderZoom(ctx) {
216
+ // Scale from 0 → full size, from center.
217
+ const { baseId, durMs, spid, direction = 'in' } = ctx;
218
+ const isOut = direction === 'out';
219
+ const fromScale = isOut ? '1.5' : '0';
220
+ const toScale = '1';
221
+ return [
222
+ setVisible(spid, baseId + 1),
223
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
224
+ attrAnim({ id: baseId + 3, durMs, spid, attrName: 'ppt_w', fromVal: fromScale, toVal: toScale, valueType: 'num' }),
225
+ attrAnim({ id: baseId + 4, durMs, spid, attrName: 'ppt_h', fromVal: fromScale, toVal: toScale, valueType: 'num' }),
226
+ ].join('');
227
+ }
228
+
229
+ function renderWipe(ctx) {
230
+ const { baseId, durMs, spid, direction = 'left' } = ctx;
231
+ return [
232
+ setVisible(spid, baseId + 1),
233
+ animEffect({ id: baseId + 2, durMs, spid, filter: wipeFilter(direction), transition: 'in' }),
234
+ ].join('');
235
+ }
236
+
237
+ function renderDissolve(ctx) {
238
+ const { baseId, durMs, spid } = ctx;
239
+ return [
240
+ setVisible(spid, baseId + 1),
241
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'dissolve', transition: 'in' }),
242
+ ].join('');
243
+ }
244
+
245
+ function renderSplit(ctx) {
246
+ const { baseId, durMs, spid, direction = 'out' } = ctx;
247
+ const filter = direction === 'in' ? 'barn(inVertical)' : 'barn(outVertical)';
248
+ return [
249
+ setVisible(spid, baseId + 1),
250
+ animEffect({ id: baseId + 2, durMs, spid, filter, transition: 'in' }),
251
+ ].join('');
252
+ }
253
+
254
+ function renderStretch(ctx) {
255
+ // Stretch = reveal with horizontal scale from 0 to 1.
256
+ const { baseId, durMs, spid } = ctx;
257
+ return [
258
+ setVisible(spid, baseId + 1),
259
+ attrAnim({ id: baseId + 2, durMs, spid, attrName: 'ppt_w', fromVal: '0', toVal: '1' }),
260
+ animEffect({ id: baseId + 3, durMs, spid, filter: 'fade', transition: 'in' }),
261
+ ].join('');
262
+ }
263
+
264
+ function renderBounce(ctx) {
265
+ // Elegant bounce entrance = vertical spring motion with fade-in.
266
+ const { baseId, durMs, spid } = ctx;
267
+ return [
268
+ setVisible(spid, baseId + 1),
269
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
270
+ // Vertical bounce: from 30% above, overshoot slightly, settle
271
+ `<p:anim calcmode="spline" keySpline="0.68,-0.55 0.265,1.55" valueType="num">
272
+ <p:cBhvr additive="base">
273
+ <p:cTn id="${baseId + 3}" dur="${durMs}" fill="hold"/>
274
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
275
+ <p:attrNameLst><p:attrName>ppt_y</p:attrName></p:attrNameLst>
276
+ </p:cBhvr>
277
+ <p:tavLst>
278
+ <p:tav tm="0"><p:val><p:strVal val="#ppt_y - #ppt_h/2"/></p:val></p:tav>
279
+ <p:tav tm="100000"><p:val><p:strVal val="#ppt_y"/></p:val></p:tav>
280
+ </p:tavLst>
281
+ </p:anim>`,
282
+ ].join('');
283
+ }
284
+
285
+ function renderSpin(ctx) {
286
+ // Full rotation + fade-in (entrance variant of spin).
287
+ const { baseId, durMs, spid } = ctx;
288
+ return [
289
+ setVisible(spid, baseId + 1),
290
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
291
+ attrAnim({ id: baseId + 3, durMs, spid, attrName: 'style.rotation', fromVal: '-180', toVal: '0', easing: 'spline' }),
292
+ ].join('');
293
+ }
294
+
295
+ function renderRiseUp(ctx) {
296
+ // Rise up = subtle slide up from below with fade. Modern "classy" reveal.
297
+ const { baseId, durMs, spid } = ctx;
298
+ return [
299
+ setVisible(spid, baseId + 1),
300
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
301
+ attrAnim({ id: baseId + 3, durMs, spid, attrName: 'ppt_y', fromVal: '#ppt_y + #ppt_h/4', toVal: '#ppt_y' }),
302
+ ].join('');
303
+ }
304
+
305
+ function renderDescend(ctx) {
306
+ const { baseId, durMs, spid } = ctx;
307
+ return [
308
+ setVisible(spid, baseId + 1),
309
+ animEffect({ id: baseId + 2, durMs, spid, filter: 'fade', transition: 'in' }),
310
+ attrAnim({ id: baseId + 3, durMs, spid, attrName: 'ppt_y', fromVal: '#ppt_y - #ppt_h/4', toVal: '#ppt_y' }),
311
+ ].join('');
312
+ }
313
+
314
+ // Fallback: pure fade-in (used for less common entrance presets)
315
+ function renderGenericEntrance(ctx) {
316
+ return renderFadein(ctx);
317
+ }
318
+
319
+ // ======================================================================
320
+ // EMPHASIS RENDERERS
321
+ // ======================================================================
322
+
323
+ function renderPulse(ctx) {
324
+ // Pulse scales 1 → 1.08 → 1 over the duration.
325
+ const { baseId, durMs, spid } = ctx;
326
+ return `
327
+ <p:anim calcmode="spline" keySpline="0.42,0 0.58,1 0.42,0 0.58,1" valueType="num">
328
+ <p:cBhvr additive="base">
329
+ <p:cTn id="${baseId + 1}" dur="${durMs}" fill="hold"/>
330
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
331
+ <p:attrNameLst><p:attrName>ppt_w</p:attrName></p:attrNameLst>
332
+ </p:cBhvr>
333
+ <p:tavLst>
334
+ <p:tav tm="0"><p:val><p:strVal val="#ppt_w"/></p:val></p:tav>
335
+ <p:tav tm="50000"><p:val><p:strVal val="#ppt_w * 1.08"/></p:val></p:tav>
336
+ <p:tav tm="100000"><p:val><p:strVal val="#ppt_w"/></p:val></p:tav>
337
+ </p:tavLst>
338
+ </p:anim>
339
+ <p:anim calcmode="spline" keySpline="0.42,0 0.58,1 0.42,0 0.58,1" valueType="num">
340
+ <p:cBhvr additive="base">
341
+ <p:cTn id="${baseId + 2}" dur="${durMs}" fill="hold"/>
342
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
343
+ <p:attrNameLst><p:attrName>ppt_h</p:attrName></p:attrNameLst>
344
+ </p:cBhvr>
345
+ <p:tavLst>
346
+ <p:tav tm="0"><p:val><p:strVal val="#ppt_h"/></p:val></p:tav>
347
+ <p:tav tm="50000"><p:val><p:strVal val="#ppt_h * 1.08"/></p:val></p:tav>
348
+ <p:tav tm="100000"><p:val><p:strVal val="#ppt_h"/></p:val></p:tav>
349
+ </p:tavLst>
350
+ </p:anim>`;
351
+ }
352
+
353
+ function renderEmphasisSpin(ctx) {
354
+ const { baseId, durMs, spid } = ctx;
355
+ return attrAnim({ id: baseId + 1, durMs, spid, attrName: 'style.rotation', fromVal: '0', toVal: '360', easing: 'lin' });
356
+ }
357
+
358
+ function renderGrow(ctx) {
359
+ // Grow 1 → 1.15
360
+ const { baseId, durMs, spid } = ctx;
361
+ return [
362
+ attrAnim({ id: baseId + 1, durMs, spid, attrName: 'ppt_w', fromVal: '#ppt_w', toVal: '#ppt_w * 1.15' }),
363
+ attrAnim({ id: baseId + 2, durMs, spid, attrName: 'ppt_h', fromVal: '#ppt_h', toVal: '#ppt_h * 1.15' }),
364
+ ].join('');
365
+ }
366
+
367
+ function renderShrink(ctx) {
368
+ const { baseId, durMs, spid } = ctx;
369
+ return [
370
+ attrAnim({ id: baseId + 1, durMs, spid, attrName: 'ppt_w', fromVal: '#ppt_w', toVal: '#ppt_w * 0.85' }),
371
+ attrAnim({ id: baseId + 2, durMs, spid, attrName: 'ppt_h', fromVal: '#ppt_h', toVal: '#ppt_h * 0.85' }),
372
+ ].join('');
373
+ }
374
+
375
+ function renderFlash(ctx) {
376
+ const { baseId, durMs, spid } = ctx;
377
+ return `
378
+ <p:anim calcmode="discrete" valueType="num">
379
+ <p:cBhvr additive="base">
380
+ <p:cTn id="${baseId + 1}" dur="${durMs}" fill="hold"/>
381
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
382
+ <p:attrNameLst><p:attrName>style.opacity</p:attrName></p:attrNameLst>
383
+ </p:cBhvr>
384
+ <p:tavLst>
385
+ <p:tav tm="0"><p:val><p:strVal val="1"/></p:val></p:tav>
386
+ <p:tav tm="25000"><p:val><p:strVal val="0.2"/></p:val></p:tav>
387
+ <p:tav tm="50000"><p:val><p:strVal val="1"/></p:val></p:tav>
388
+ <p:tav tm="75000"><p:val><p:strVal val="0.2"/></p:val></p:tav>
389
+ <p:tav tm="100000"><p:val><p:strVal val="1"/></p:val></p:tav>
390
+ </p:tavLst>
391
+ </p:anim>`;
392
+ }
393
+
394
+ function renderTeeter(ctx) {
395
+ // Teeter = tilt back and forth around center (+8°, -8°, +4°, 0°)
396
+ const { baseId, durMs, spid } = ctx;
397
+ return `
398
+ <p:anim calcmode="spline" keySpline="0.4,0 0.6,1 0.4,0 0.6,1 0.4,0 0.6,1 0.4,0 0.6,1" valueType="num">
399
+ <p:cBhvr additive="base">
400
+ <p:cTn id="${baseId + 1}" dur="${durMs}" fill="hold"/>
401
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
402
+ <p:attrNameLst><p:attrName>style.rotation</p:attrName></p:attrNameLst>
403
+ </p:cBhvr>
404
+ <p:tavLst>
405
+ <p:tav tm="0"><p:val><p:strVal val="0"/></p:val></p:tav>
406
+ <p:tav tm="25000"><p:val><p:strVal val="8"/></p:val></p:tav>
407
+ <p:tav tm="50000"><p:val><p:strVal val="-8"/></p:val></p:tav>
408
+ <p:tav tm="75000"><p:val><p:strVal val="4"/></p:val></p:tav>
409
+ <p:tav tm="100000"><p:val><p:strVal val="0"/></p:val></p:tav>
410
+ </p:tavLst>
411
+ </p:anim>`;
412
+ }
413
+
414
+ function renderBlink(ctx) {
415
+ const { baseId, durMs, spid } = ctx;
416
+ return `
417
+ <p:anim calcmode="discrete" valueType="num">
418
+ <p:cBhvr additive="base">
419
+ <p:cTn id="${baseId + 1}" dur="${durMs}" fill="hold"/>
420
+ <p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>
421
+ <p:attrNameLst><p:attrName>style.opacity</p:attrName></p:attrNameLst>
422
+ </p:cBhvr>
423
+ <p:tavLst>
424
+ <p:tav tm="0"><p:val><p:strVal val="1"/></p:val></p:tav>
425
+ <p:tav tm="50000"><p:val><p:strVal val="0"/></p:val></p:tav>
426
+ <p:tav tm="100000"><p:val><p:strVal val="1"/></p:val></p:tav>
427
+ </p:tavLst>
428
+ </p:anim>`;
429
+ }
430
+
431
+ // Fallback for less common emphasis effects
432
+ function renderGenericEmphasis(ctx) {
433
+ return renderPulse(ctx);
434
+ }
435
+
436
+ // ======================================================================
437
+ // EXIT RENDERERS
438
+ // ======================================================================
439
+
440
+ function renderFadeout(ctx) {
441
+ const { baseId, durMs, spid } = ctx;
442
+ return [
443
+ animEffect({ id: baseId + 1, durMs, spid, filter: 'fade', transition: 'out' }),
444
+ setHidden(spid, baseId + 2),
445
+ ].join('');
446
+ }
447
+
448
+ function renderDisappear(ctx) {
449
+ const { baseId, spid } = ctx;
450
+ return setHidden(spid, baseId + 1);
451
+ }
452
+
453
+ function renderFlyout(ctx) {
454
+ const { baseId, durMs, spid, direction = 'left' } = ctx;
455
+ const fly = flyToForExit(direction);
456
+ return [
457
+ attrAnim({ id: baseId + 1, durMs, spid, attrName: fly.attr, fromVal: fly.from, toVal: fly.to }),
458
+ setHidden(spid, baseId + 2),
459
+ ].join('');
460
+ }
461
+
462
+ function renderZoomout(ctx) {
463
+ const { baseId, durMs, spid, direction = 'out' } = ctx;
464
+ const toScale = direction === 'in' ? '1.5' : '0';
465
+ return [
466
+ animEffect({ id: baseId + 1, durMs, spid, filter: 'fade', transition: 'out' }),
467
+ attrAnim({ id: baseId + 2, durMs, spid, attrName: 'ppt_w', fromVal: '1', toVal: toScale }),
468
+ attrAnim({ id: baseId + 3, durMs, spid, attrName: 'ppt_h', fromVal: '1', toVal: toScale }),
469
+ setHidden(spid, baseId + 4),
470
+ ].join('');
471
+ }
472
+
473
+ function renderWipeout(ctx) {
474
+ const { baseId, durMs, spid, direction = 'left' } = ctx;
475
+ return [
476
+ animEffect({ id: baseId + 1, durMs, spid, filter: wipeFilter(direction), transition: 'out' }),
477
+ setHidden(spid, baseId + 2),
478
+ ].join('');
479
+ }
480
+
481
+ function renderDissolveout(ctx) {
482
+ const { baseId, durMs, spid } = ctx;
483
+ return [
484
+ animEffect({ id: baseId + 1, durMs, spid, filter: 'dissolve', transition: 'out' }),
485
+ setHidden(spid, baseId + 2),
486
+ ].join('');
487
+ }
488
+
489
+ function renderGenericExit(ctx) {
490
+ return renderFadeout(ctx);
491
+ }
492
+
493
+ // ======================================================================
494
+ // REGISTRY
495
+ // ======================================================================
496
+ //
497
+ // Maps entrance/emphasis/exit preset names → renderer function.
498
+ // Presets not in the registry fall through to a sensible generic renderer.
499
+
500
+ export const PRESET_RENDERERS = {
501
+ // --- Entrance ---
502
+ appear: renderAppear,
503
+ fadein: renderFadein,
504
+ flyin: renderFlyin,
505
+ floatin: renderFloatin,
506
+ split: renderSplit,
507
+ wipe: renderWipe,
508
+ zoom: renderZoom,
509
+ bounce: renderBounce,
510
+ swivel: renderSpin,
511
+ dissolvein: renderDissolve,
512
+ expand: renderStretch,
513
+ stretch: renderStretch,
514
+ riseup: renderRiseUp,
515
+ ascend: renderRiseUp,
516
+ descend: renderDescend,
517
+ risefall: renderDescend,
518
+ float: renderFloatin,
519
+ glidein: renderFadein,
520
+ fadeswivel: renderSpin,
521
+ zoomcenter: renderZoom,
522
+ basiczoom: renderZoom,
523
+ fadedzoom: renderZoom,
524
+ compress: renderZoom,
525
+ centerrevolve: renderSpin,
526
+ spinner: renderSpin,
527
+ pinwheel: renderSpin,
528
+ boomerang: renderBounce,
529
+ credits: renderRiseUp,
530
+ curveup: renderRiseUp,
531
+ flipin: renderSpin,
532
+ foldin: renderStretch,
533
+ gridout: renderWipe,
534
+ spiral: renderSpin,
535
+ thread: renderFadein,
536
+ whip: renderFlyin,
537
+ // --- Emphasis ---
538
+ pulse: renderPulse,
539
+ spin: renderEmphasisSpin,
540
+ grow: renderGrow,
541
+ shrink: renderShrink,
542
+ flash: renderFlash,
543
+ colorpulse: renderPulse,
544
+ teeter: renderTeeter,
545
+ shimmer: renderPulse,
546
+ blink: renderBlink,
547
+ bold: renderPulse,
548
+ wave: renderTeeter,
549
+ desaturate: renderPulse,
550
+ darken: renderPulse,
551
+ lighten: renderPulse,
552
+ transparent: renderPulse,
553
+ colorwave: renderPulse,
554
+ brushon: renderPulse,
555
+ wobble: renderTeeter,
556
+ // --- Exit ---
557
+ disappear: renderDisappear,
558
+ fadeout: renderFadeout,
559
+ flyout: renderFlyout,
560
+ floatout: renderFadeout,
561
+ zoomout: renderZoomout,
562
+ splitout: renderFadeout,
563
+ wipeout: renderWipeout,
564
+ shrinkout: renderZoomout,
565
+ dissolveout: renderDissolveout,
566
+ peekout: renderFadeout,
567
+ bounceout: renderFadeout,
568
+ swivelout: renderFadeout,
569
+ spiralout: renderFadeout,
570
+ flipout: renderFadeout,
571
+ foldout: renderFadeout,
572
+ glideout: renderFadeout,
573
+ boomerangout: renderFadeout,
574
+ contract: renderZoomout,
575
+ };
576
+
577
+ export function renderPresetChildTimeline(name, ctx) {
578
+ const renderer = PRESET_RENDERERS[name];
579
+ if (renderer) return renderer(ctx);
580
+ // Fallback — sensible default per preset class
581
+ if (ctx.presetClass === 'exit') return renderGenericExit(ctx);
582
+ if (ctx.presetClass === 'emph') return renderGenericEmphasis(ctx);
583
+ return renderGenericEntrance(ctx);
584
+ }