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.
- package/app/docs/content.js +2082 -0
- package/cli/dist/commands/config-show.d.ts +1 -0
- package/cli/dist/commands/config-show.js +16 -0
- package/cli/dist/commands/convert.d.ts +10 -0
- package/cli/dist/commands/convert.js +311 -0
- package/cli/dist/commands/edit.d.ts +33 -0
- package/cli/dist/commands/edit.js +588 -0
- package/cli/dist/commands/init.d.ts +1 -0
- package/cli/dist/commands/init.js +35 -0
- package/cli/dist/commands/logout.d.ts +1 -0
- package/cli/dist/commands/logout.js +19 -0
- package/cli/dist/commands/publish.d.ts +10 -0
- package/cli/dist/commands/publish.js +17 -0
- package/cli/dist/commands/status.d.ts +5 -0
- package/cli/dist/commands/status.js +71 -0
- package/cli/dist/commands/templates.d.ts +13 -0
- package/cli/dist/commands/templates.js +85 -0
- package/cli/dist/commands/whoami.d.ts +5 -0
- package/cli/dist/commands/whoami.js +51 -0
- package/cli/dist/config.d.ts +7 -0
- package/cli/dist/config.js +24 -0
- package/cli/dist/index.d.ts +2 -0
- package/cli/dist/index.js +93 -0
- package/cli/dist/update-check.d.ts +1 -0
- package/cli/dist/update-check.js +30 -0
- package/cli/package.json +46 -0
- package/lib/local-slide-editor-launcher.js +353 -0
- package/lib/pptx-studio-mcp-core.js +1744 -0
- package/lib/server/template-html-policy.mjs +354 -0
- package/mcp/pptx-studio-mcp-server.mjs +198 -0
- package/package.json +32 -0
- package/scripts/install-mcp.mjs +316 -0
- package/src/animation-injector.js +724 -0
- package/src/animation-renderers.js +584 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
// src/animation-injector.js
|
|
2
|
+
//
|
|
3
|
+
// Post-processes a generated PPTX ZIP by injecting <p:transition> and <p:timing>
|
|
4
|
+
// elements into each slide's XML, based on data-anim-* attributes found in the
|
|
5
|
+
// source HTML.
|
|
6
|
+
//
|
|
7
|
+
// DSL (declarative, AOS-style):
|
|
8
|
+
//
|
|
9
|
+
// <section class="slide"
|
|
10
|
+
// data-transition="fade"
|
|
11
|
+
// data-transition-duration="800">
|
|
12
|
+
// ...
|
|
13
|
+
// <h1 data-anim="flyin"
|
|
14
|
+
// data-anim-direction="left"
|
|
15
|
+
// data-anim-duration="1000"
|
|
16
|
+
// data-anim-delay="200"
|
|
17
|
+
// data-anim-trigger="afterPrevious">Title</h1>
|
|
18
|
+
// </section>
|
|
19
|
+
//
|
|
20
|
+
// Design goals:
|
|
21
|
+
// - Zero-effect on HTML that has no data-anim-* (backward compatible)
|
|
22
|
+
// - Works as a PostProcess on the ZIP produced by PptxGenJS, no upstream changes
|
|
23
|
+
// - Keeps the authored text / shape editable — we only append <p:timing> nodes
|
|
24
|
+
//
|
|
25
|
+
// This is a deliberately minimal MVP that supports slide transitions and a
|
|
26
|
+
// handful of entrance animations. Extend `ENTRANCE_PRESETS` and `TRANSITION_PRESETS`
|
|
27
|
+
// to grow the catalogue.
|
|
28
|
+
|
|
29
|
+
/* ----------------------------------------------------------------------
|
|
30
|
+
* PRESETS — OOXML <p:transition> children
|
|
31
|
+
* See http://officeopenxml.com/prSlide-transitions.php
|
|
32
|
+
* ---------------------------------------------------------------------- */
|
|
33
|
+
// Each factory returns the inner XML for the given <p:transition>.
|
|
34
|
+
// Called with a `direction` parameter from data-transition-direction (optional).
|
|
35
|
+
// Modern transitions (morph / vortex / ferris / gallery / etc.) require the
|
|
36
|
+
// Office 2016+ extension list and use the p16 namespace. We emit them as
|
|
37
|
+
// <p:extLst> children so they render in PowerPoint 2016+ and gracefully
|
|
38
|
+
// fall back to no-transition in older viewers.
|
|
39
|
+
const TRANSITION_PRESETS = {
|
|
40
|
+
none: () => '',
|
|
41
|
+
fade: () => '<p:fade/>',
|
|
42
|
+
push: (dir = 'left') => `<p:push dir="${dirShort(dir, 'l')}"/>`,
|
|
43
|
+
wipe: (dir = 'left') => `<p:wipe dir="${dirShort(dir, 'l')}"/>`,
|
|
44
|
+
cover: (dir = 'left') => `<p:cover dir="${dirShort(dir, 'l')}"/>`,
|
|
45
|
+
uncover: (dir = 'left') => `<p:pull dir="${dirShort(dir, 'l')}"/>`,
|
|
46
|
+
split: (dir = 'out') => `<p:split orient="horz" dir="${dir === 'in' ? 'in' : 'out'}"/>`,
|
|
47
|
+
cut: () => '<p:cut/>',
|
|
48
|
+
dissolve: () => '<p:dissolve/>',
|
|
49
|
+
zoom: () => '<p:fade/>', // legacy fallback
|
|
50
|
+
random: () => '<p:random/>',
|
|
51
|
+
|
|
52
|
+
// --- Office 2016+ modern transitions via extLst ---
|
|
53
|
+
// ref: [MS-PPT] §2.5.129 / ECMA-376 Part 4 §13.11
|
|
54
|
+
morph: () => modernTransitionExt('morph', 'byObject'),
|
|
55
|
+
vortex: (dir = 'left') => modernTransitionExt('vortex', null, { dir: dirShort(dir, 'l') }),
|
|
56
|
+
ferris: (dir = 'left') => modernTransitionExt('ferris', null, { dir: dirShort(dir, 'l') }),
|
|
57
|
+
gallery: (dir = 'left') => modernTransitionExt('gallery', null, { dir: dirShort(dir, 'l') }),
|
|
58
|
+
conveyor: (dir = 'left') => modernTransitionExt('conveyor', null, { dir: dirShort(dir, 'l') }),
|
|
59
|
+
flash: () => modernTransitionExt('flash'),
|
|
60
|
+
prism: (dir = 'left') => modernTransitionExt('prism', null, { dir: dirShort(dir, 'l') }),
|
|
61
|
+
glitter: (dir = 'left') => modernTransitionExt('glitter', null, { dir: dirShort(dir, 'l') }),
|
|
62
|
+
honeycomb: () => modernTransitionExt('honeycomb'),
|
|
63
|
+
warp: (dir = 'in') => modernTransitionExt('warp', null, { dir: dir === 'in' ? 'in' : 'out' }),
|
|
64
|
+
window: (dir = 'in') => modernTransitionExt('window', null, { dir: dir === 'in' ? 'in' : 'out' }),
|
|
65
|
+
orbit: () => modernTransitionExt('orbit'),
|
|
66
|
+
shred: (dir = 'in') => modernTransitionExt('shred', null, { dir: dir === 'in' ? 'in' : 'out' }),
|
|
67
|
+
switch: (dir = 'left') => modernTransitionExt('switch', null, { dir: dirShort(dir, 'l') }),
|
|
68
|
+
flip: (dir = 'left') => modernTransitionExt('flip', null, { dir: dirShort(dir, 'l') }),
|
|
69
|
+
cube: (dir = 'left') => modernTransitionExt('cube', null, { dir: dirShort(dir, 'l') }),
|
|
70
|
+
doors: (dir = 'horz') => modernTransitionExt('doors', null, { dir: dir === 'horz' ? 'horz' : 'vert' }),
|
|
71
|
+
box: (dir = 'in') => modernTransitionExt('box', null, { dir: dir === 'in' ? 'in' : 'out' }),
|
|
72
|
+
rotate: () => modernTransitionExt('rotate'),
|
|
73
|
+
revealSmoothly: () => modernTransitionExt('revealSmoothly'),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function modernTransitionExt(name, option = null, attrs = {}) {
|
|
77
|
+
const attrStr = Object.entries(attrs)
|
|
78
|
+
.map(([k, v]) => `${k}="${escapeXmlAttr(v)}"`)
|
|
79
|
+
.join(' ');
|
|
80
|
+
const optionAttr = option ? ` option="${escapeXmlAttr(option)}"` : '';
|
|
81
|
+
return [
|
|
82
|
+
'<p:extLst>',
|
|
83
|
+
`<p:ext uri="{E01B4FDE-8C12-4F1D-8B9C-1E2C5E4AB7E1}">`,
|
|
84
|
+
`<p14:${name} xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main"${attrStr ? ' ' + attrStr : ''}${optionAttr}/>`,
|
|
85
|
+
'</p:ext>',
|
|
86
|
+
'</p:extLst>',
|
|
87
|
+
].join('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function dirShort(dir, fallback) {
|
|
91
|
+
const m = { left: 'l', right: 'r', up: 'u', down: 'd' };
|
|
92
|
+
return m[dir] || fallback;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
import { renderPresetChildTimeline } from './animation-renderers.js';
|
|
96
|
+
|
|
97
|
+
function escapeXmlAttr(s) {
|
|
98
|
+
return String(s)
|
|
99
|
+
.replace(/&/g, '&')
|
|
100
|
+
.replace(/"/g, '"')
|
|
101
|
+
.replace(/</g, '<')
|
|
102
|
+
.replace(/>/g, '>');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ----------------------------------------------------------------------
|
|
106
|
+
* PRESETS — Entrance/Emphasis/Exit animations
|
|
107
|
+
*
|
|
108
|
+
* presetID values follow ECMA-376 §19.5 and the Microsoft animation schema
|
|
109
|
+
* https://learn.microsoft.com/en-us/office/open-xml/presentation/working-with-animation
|
|
110
|
+
*
|
|
111
|
+
* Where the spec is ambiguous we use the mapping PowerPoint 2016+ writes
|
|
112
|
+
* out when you pick the effect manually.
|
|
113
|
+
* ---------------------------------------------------------------------- */
|
|
114
|
+
const ENTRANCE_PRESETS = {
|
|
115
|
+
// --- Basic ---
|
|
116
|
+
appear: { presetID: 1, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
117
|
+
fadein: { presetID: 10, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
118
|
+
flyin: { presetID: 2, presetClass: 'entr', presetSubtype: 4, directional: 'cardinal' },
|
|
119
|
+
floatin: { presetID: 42, presetClass: 'entr', presetSubtype: 0, directional: 'cardinal' },
|
|
120
|
+
split: { presetID: 3, presetClass: 'entr', presetSubtype: 26, directional: 'inout' },
|
|
121
|
+
wipe: { presetID: 4, presetClass: 'entr', presetSubtype: 4, directional: 'cardinal' },
|
|
122
|
+
zoom: { presetID: 23, presetClass: 'entr', presetSubtype: 0, directional: 'inout' },
|
|
123
|
+
bounce: { presetID: 26, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
124
|
+
swivel: { presetID: 18, presetClass: 'entr', presetSubtype: 1, directional: false },
|
|
125
|
+
// --- Subtle ---
|
|
126
|
+
dissolvein: { presetID: 9, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
127
|
+
expand: { presetID: 13, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
128
|
+
fadeswivel: { presetID: 45, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
129
|
+
zoomcenter: { presetID: 23, presetClass: 'entr', presetSubtype: 16, directional: false },
|
|
130
|
+
// --- Moderate ---
|
|
131
|
+
ascend: { presetID: 39, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
132
|
+
basiczoom: { presetID: 23, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
133
|
+
centerrevolve: { presetID: 12, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
134
|
+
compress: { presetID: 21, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
135
|
+
descend: { presetID: 40, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
136
|
+
fadedzoom: { presetID: 23, presetClass: 'entr', presetSubtype: 16, directional: false },
|
|
137
|
+
gridout: { presetID: 4, presetClass: 'entr', presetSubtype: 1, directional: false },
|
|
138
|
+
risefall: { presetID: 49, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
139
|
+
riseup: { presetID: 43, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
140
|
+
spinner: { presetID: 17, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
141
|
+
stretch: { presetID: 46, presetClass: 'entr', presetSubtype: 4, directional: 'cardinal' },
|
|
142
|
+
// --- Exciting ---
|
|
143
|
+
boomerang: { presetID: 25, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
144
|
+
credits: { presetID: 28, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
145
|
+
curveup: { presetID: 15, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
146
|
+
flipin: { presetID: 20, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
147
|
+
float: { presetID: 42, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
148
|
+
foldin: { presetID: 7, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
149
|
+
glidein: { presetID: 19, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
150
|
+
pinwheel: { presetID: 38, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
151
|
+
spiral: { presetID: 27, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
152
|
+
thread: { presetID: 30, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
153
|
+
whip: { presetID: 32, presetClass: 'entr', presetSubtype: 0, directional: false },
|
|
154
|
+
|
|
155
|
+
// ================================================================
|
|
156
|
+
// EMPHASIS
|
|
157
|
+
// ================================================================
|
|
158
|
+
pulse: { presetID: 1, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
159
|
+
spin: { presetID: 8, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
160
|
+
grow: { presetID: 6, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
161
|
+
shrink: { presetID: 6, presetClass: 'emph', presetSubtype: 1, directional: false },
|
|
162
|
+
flash: { presetID: 14, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
163
|
+
colorpulse: { presetID: 2, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
164
|
+
teeter: { presetID: 3, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
165
|
+
shimmer: { presetID: 24, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
166
|
+
blink: { presetID: 20, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
167
|
+
bold: { presetID: 9, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
168
|
+
wave: { presetID: 37, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
169
|
+
desaturate: { presetID: 13, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
170
|
+
darken: { presetID: 11, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
171
|
+
lighten: { presetID: 17, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
172
|
+
transparent: { presetID: 19, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
173
|
+
colorwave: { presetID: 36, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
174
|
+
brushon: { presetID: 35, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
175
|
+
wobble: { presetID: 21, presetClass: 'emph', presetSubtype: 0, directional: false },
|
|
176
|
+
|
|
177
|
+
// ================================================================
|
|
178
|
+
// EXIT
|
|
179
|
+
// ================================================================
|
|
180
|
+
disappear: { presetID: 1, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
181
|
+
fadeout: { presetID: 10, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
182
|
+
flyout: { presetID: 2, presetClass: 'exit', presetSubtype: 4, directional: 'cardinal' },
|
|
183
|
+
floatout: { presetID: 42, presetClass: 'exit', presetSubtype: 0, directional: 'cardinal' },
|
|
184
|
+
zoomout: { presetID: 23, presetClass: 'exit', presetSubtype: 0, directional: 'inout' },
|
|
185
|
+
splitout: { presetID: 3, presetClass: 'exit', presetSubtype: 26, directional: 'inout' },
|
|
186
|
+
wipeout: { presetID: 4, presetClass: 'exit', presetSubtype: 4, directional: 'cardinal' },
|
|
187
|
+
shrinkout: { presetID: 6, presetClass: 'exit', presetSubtype: 1, directional: false },
|
|
188
|
+
dissolveout: { presetID: 9, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
189
|
+
peekout: { presetID: 24, presetClass: 'exit', presetSubtype: 4, directional: 'cardinal' },
|
|
190
|
+
bounceout: { presetID: 26, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
191
|
+
swivelout: { presetID: 18, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
192
|
+
spiralout: { presetID: 27, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
193
|
+
flipout: { presetID: 20, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
194
|
+
foldout: { presetID: 7, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
195
|
+
glideout: { presetID: 19, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
196
|
+
boomerangout: { presetID: 25, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
197
|
+
contract: { presetID: 13, presetClass: 'exit', presetSubtype: 0, directional: false },
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Cardinal direction → subtype mapping (OOXML presetSubtype values).
|
|
201
|
+
// Used by flyin / floatin / wipe / flyout where "from the left", etc. matters.
|
|
202
|
+
const CARDINAL_SUBTYPES = {
|
|
203
|
+
left: 4,
|
|
204
|
+
right: 8,
|
|
205
|
+
up: 1,
|
|
206
|
+
down: 2,
|
|
207
|
+
topleft: 5,
|
|
208
|
+
topright: 9,
|
|
209
|
+
bottomleft: 6,
|
|
210
|
+
bottomright: 10,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// In/Out direction → subtype for split / zoom / zoomout.
|
|
214
|
+
const INOUT_SUBTYPES = {
|
|
215
|
+
in: 16,
|
|
216
|
+
out: 32,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/* ----------------------------------------------------------------------
|
|
220
|
+
* MOTION PATHS — <p:animMotion path="..." />
|
|
221
|
+
*
|
|
222
|
+
* Each entry returns the SVG-style path string expected by PowerPoint.
|
|
223
|
+
* Coordinates are in the 0..1 space (relative to the slide). The value
|
|
224
|
+
* accepted by PowerPoint is a mini SVG path DSL with "M"/"L"/"C"/"E" commands.
|
|
225
|
+
*
|
|
226
|
+
* Docs: https://learn.microsoft.com/en-us/office/open-xml/presentation/working-with-animation
|
|
227
|
+
* ---------------------------------------------------------------------- */
|
|
228
|
+
const MOTION_PATHS = {
|
|
229
|
+
line: 'M 0 0 L 0.2 0 E',
|
|
230
|
+
lineright: 'M 0 0 L 0.3 0 E',
|
|
231
|
+
lineleft: 'M 0 0 L -0.3 0 E',
|
|
232
|
+
lineup: 'M 0 0 L 0 -0.3 E',
|
|
233
|
+
linedown: 'M 0 0 L 0 0.3 E',
|
|
234
|
+
arc: 'M 0 0 C 0.1 -0.1 0.2 -0.1 0.3 0 E',
|
|
235
|
+
arcup: 'M 0 0 C 0.1 -0.2 0.2 -0.2 0.3 0 E',
|
|
236
|
+
arcdown: 'M 0 0 C 0.1 0.2 0.2 0.2 0.3 0 E',
|
|
237
|
+
curve: 'M 0 0 C 0.05 -0.1 0.15 0.1 0.3 0 E',
|
|
238
|
+
scurve: 'M 0 0 C 0.1 -0.15 0.2 0.15 0.3 0 E',
|
|
239
|
+
circle: 'M 0 0 C 0.1 0 0.2 0.1 0.2 0.2 C 0.2 0.3 0.1 0.4 0 0.4 C -0.1 0.4 -0.2 0.3 -0.2 0.2 C -0.2 0.1 -0.1 0 0 0 E',
|
|
240
|
+
square: 'M 0 0 L 0.2 0 L 0.2 0.2 L 0 0.2 L 0 0 E',
|
|
241
|
+
diamond: 'M 0 0 L 0.15 -0.15 L 0.3 0 L 0.15 0.15 L 0 0 E',
|
|
242
|
+
triangle: 'M 0 0 L 0.15 -0.2 L 0.3 0 L 0 0 E',
|
|
243
|
+
bounce: 'M 0 0 C 0.05 -0.15 0.1 0 0.1 0 C 0.15 -0.08 0.2 0 0.2 0 C 0.25 -0.04 0.3 0 0.3 0 E',
|
|
244
|
+
loop: 'M 0 0 C 0.1 -0.2 0.3 -0.2 0.2 0 C 0.1 0.15 -0.1 0.15 0 0 L 0.3 0 E',
|
|
245
|
+
zigzag: 'M 0 0 L 0.05 -0.1 L 0.1 0.1 L 0.15 -0.1 L 0.2 0.1 L 0.25 0 E',
|
|
246
|
+
wave: 'M 0 0 C 0.05 -0.1 0.1 -0.1 0.15 0 C 0.2 0.1 0.25 0.1 0.3 0 E',
|
|
247
|
+
spiral: 'M 0 0 C 0.05 -0.05 0.1 0 0.05 0.05 C -0.05 0.1 -0.1 0 0 -0.05 L 0.2 -0.05 E',
|
|
248
|
+
figure8: 'M 0 0 C -0.1 -0.1 -0.1 0.1 0 0 C 0.1 -0.1 0.1 0.1 0 0 E',
|
|
249
|
+
pointyright: 'M 0 0 L 0.1 -0.05 L 0.2 0 L 0.1 0.05 L 0 0 E',
|
|
250
|
+
decayingwave: 'M 0 0 C 0.03 -0.12 0.06 0.12 0.09 -0.08 C 0.12 0.08 0.15 -0.04 0.18 0.04 C 0.21 0 0.24 0 0.3 0 E',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function resolveSubtype(preset, direction) {
|
|
254
|
+
if (!preset.directional || !direction) return preset.presetSubtype;
|
|
255
|
+
if (preset.directional === 'cardinal' && CARDINAL_SUBTYPES[direction] != null) {
|
|
256
|
+
return CARDINAL_SUBTYPES[direction];
|
|
257
|
+
}
|
|
258
|
+
if (preset.directional === 'inout' && INOUT_SUBTYPES[direction] != null) {
|
|
259
|
+
return INOUT_SUBTYPES[direction];
|
|
260
|
+
}
|
|
261
|
+
return preset.presetSubtype;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ----------------------------------------------------------------------
|
|
265
|
+
* extractAnimationPlan
|
|
266
|
+
* Walk the source HTML and collect per-slide animation descriptors.
|
|
267
|
+
*
|
|
268
|
+
* Returns:
|
|
269
|
+
* [
|
|
270
|
+
* {
|
|
271
|
+
* slideIndex: 0,
|
|
272
|
+
* transition: { type, duration } | null,
|
|
273
|
+
* entries: [{ shapeId, type, direction, duration, delay, trigger, presetInfo }, ...]
|
|
274
|
+
* },
|
|
275
|
+
* ...
|
|
276
|
+
* ]
|
|
277
|
+
*
|
|
278
|
+
* Note: shapeId matching is the hard part. PptxGenJS generates shape IDs in the
|
|
279
|
+
* order it processes elements. We assign `data-pptx-shape-index` to each text/
|
|
280
|
+
* shape element before PPTX generation and read that back here.
|
|
281
|
+
* ---------------------------------------------------------------------- */
|
|
282
|
+
export function extractAnimationPlan(rootElement) {
|
|
283
|
+
const plans = [];
|
|
284
|
+
const slides = rootElement.querySelectorAll('section.slide');
|
|
285
|
+
|
|
286
|
+
slides.forEach((slide, slideIndex) => {
|
|
287
|
+
const plan = { slideIndex, transition: null, entries: [] };
|
|
288
|
+
|
|
289
|
+
// --- transition ---
|
|
290
|
+
const tType = slide.getAttribute('data-transition');
|
|
291
|
+
if (tType && TRANSITION_PRESETS[tType] != null) {
|
|
292
|
+
plan.transition = {
|
|
293
|
+
type: tType,
|
|
294
|
+
duration: parseInt(slide.getAttribute('data-transition-duration'), 10) || 800,
|
|
295
|
+
direction: slide.getAttribute('data-transition-direction') || null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- element animations ---
|
|
300
|
+
const animNodes = slide.querySelectorAll('[data-anim]');
|
|
301
|
+
animNodes.forEach((node, localIndex) => {
|
|
302
|
+
const type = node.getAttribute('data-anim');
|
|
303
|
+
const preset = ENTRANCE_PRESETS[type];
|
|
304
|
+
if (!preset) {
|
|
305
|
+
console.warn(`[animation-injector] Unknown data-anim value: "${type}"`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const direction = node.getAttribute('data-anim-direction');
|
|
309
|
+
const presetSubtype = resolveSubtype(preset, direction);
|
|
310
|
+
|
|
311
|
+
plan.entries.push({
|
|
312
|
+
localIndex,
|
|
313
|
+
type,
|
|
314
|
+
direction,
|
|
315
|
+
duration: parseInt(node.getAttribute('data-anim-duration'), 10) || 500,
|
|
316
|
+
delay: parseInt(node.getAttribute('data-anim-delay'), 10) || 0,
|
|
317
|
+
// Default to "afterPrevious" so animations auto-cascade in the
|
|
318
|
+
// slideshow instead of requiring a click per element. Authors that
|
|
319
|
+
// want click-gated reveals can set data-anim-trigger="onClick".
|
|
320
|
+
trigger: node.getAttribute('data-anim-trigger') || 'afterPrevious',
|
|
321
|
+
presetID: preset.presetID,
|
|
322
|
+
presetClass: preset.presetClass,
|
|
323
|
+
presetSubtype,
|
|
324
|
+
kind: 'preset',
|
|
325
|
+
shapeIndex: parseInt(node.getAttribute('data-pptx-shape-index'), 10),
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// --- text-level animations (per character / word / paragraph reveal) ---
|
|
330
|
+
// Carried on a normal element animated with data-anim="fadein" (or any
|
|
331
|
+
// other entrance effect). Adding data-anim-text="bychar" makes PowerPoint
|
|
332
|
+
// apply the effect to each text run in sequence.
|
|
333
|
+
// data-anim-text="bychar" — letter by letter
|
|
334
|
+
// data-anim-text="byword" — word by word
|
|
335
|
+
// data-anim-text="byparagraph" — line/paragraph by line
|
|
336
|
+
// Optional pacing (inter-letter delay, in ms):
|
|
337
|
+
// data-anim-text-pace="80"
|
|
338
|
+
plan.entries.forEach((entry) => {
|
|
339
|
+
if (entry.kind !== 'preset') return;
|
|
340
|
+
// Find the originating DOM node to read its text-build attributes
|
|
341
|
+
const animNodes = slide.querySelectorAll('[data-anim]');
|
|
342
|
+
const node = animNodes[entry.localIndex];
|
|
343
|
+
if (!node) return;
|
|
344
|
+
const build = node.getAttribute('data-anim-text');
|
|
345
|
+
const pace = parseInt(node.getAttribute('data-anim-text-pace'), 10);
|
|
346
|
+
if (build) {
|
|
347
|
+
entry.textBuild = build; // 'bychar' | 'byword' | 'byparagraph'
|
|
348
|
+
if (Number.isFinite(pace) && pace >= 0) entry.textPaceMs = pace;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// --- motion paths ---
|
|
353
|
+
const motionNodes = slide.querySelectorAll('[data-anim-motion]');
|
|
354
|
+
motionNodes.forEach((node, localIndex) => {
|
|
355
|
+
const pathName = node.getAttribute('data-anim-motion');
|
|
356
|
+
const customPath = node.getAttribute('data-anim-motion-path');
|
|
357
|
+
const path = customPath || MOTION_PATHS[pathName];
|
|
358
|
+
if (!path) {
|
|
359
|
+
console.warn(`[animation-injector] Unknown data-anim-motion value: "${pathName}"`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
plan.entries.push({
|
|
363
|
+
localIndex: plan.entries.length + localIndex,
|
|
364
|
+
type: `motion:${pathName || 'custom'}`,
|
|
365
|
+
duration: parseInt(node.getAttribute('data-anim-duration'), 10) || 1000,
|
|
366
|
+
delay: parseInt(node.getAttribute('data-anim-delay'), 10) || 0,
|
|
367
|
+
// Default to "afterPrevious" so animations auto-cascade in the
|
|
368
|
+
// slideshow instead of requiring a click per element. Authors that
|
|
369
|
+
// want click-gated reveals can set data-anim-trigger="onClick".
|
|
370
|
+
trigger: node.getAttribute('data-anim-trigger') || 'afterPrevious',
|
|
371
|
+
kind: 'motion',
|
|
372
|
+
motionPath: path,
|
|
373
|
+
shapeIndex: parseInt(node.getAttribute('data-pptx-shape-index'), 10),
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (plan.transition || plan.entries.length > 0) {
|
|
378
|
+
plans.push(plan);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return plans;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* ----------------------------------------------------------------------
|
|
386
|
+
* buildTransitionXml — minimal <p:transition> snippet
|
|
387
|
+
* ---------------------------------------------------------------------- */
|
|
388
|
+
function buildTransitionXml(transition) {
|
|
389
|
+
if (!transition) return '';
|
|
390
|
+
const factory = TRANSITION_PRESETS[transition.type];
|
|
391
|
+
const inner = typeof factory === 'function' ? factory(transition.direction) : '';
|
|
392
|
+
const dur = Math.max(100, Math.min(5000, transition.duration));
|
|
393
|
+
// spd attribute: 'slow' | 'med' | 'fast' — derive from duration
|
|
394
|
+
let spd = 'med';
|
|
395
|
+
if (dur < 500) spd = 'fast';
|
|
396
|
+
else if (dur > 1500) spd = 'slow';
|
|
397
|
+
return `<p:transition spd="${spd}" advClick="1">${inner}</p:transition>`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* ----------------------------------------------------------------------
|
|
401
|
+
* buildTimingXml — minimal <p:timing> block for a list of entries
|
|
402
|
+
* Schema reference: ECMA-376 §19.5.76
|
|
403
|
+
* ---------------------------------------------------------------------- */
|
|
404
|
+
function buildTimingXml(entries) {
|
|
405
|
+
if (!entries.length) return '';
|
|
406
|
+
|
|
407
|
+
// One <p:par> per top-level sequence. Each entry is wrapped in its own
|
|
408
|
+
// <p:par> inside the main sequence, with presetID applied.
|
|
409
|
+
const parList = entries
|
|
410
|
+
.map((e, idx) => {
|
|
411
|
+
// nodeType depends on trigger
|
|
412
|
+
// onClick → tn id=... nodeType="clickEffect"
|
|
413
|
+
// withPrevious → tn id=... nodeType="withEffect"
|
|
414
|
+
// afterPrevious → tn id=... nodeType="afterEffect"
|
|
415
|
+
const nodeType =
|
|
416
|
+
e.trigger === 'withPrevious'
|
|
417
|
+
? 'withEffect'
|
|
418
|
+
: e.trigger === 'afterPrevious'
|
|
419
|
+
? 'afterEffect'
|
|
420
|
+
: 'clickEffect';
|
|
421
|
+
|
|
422
|
+
const durMs = e.duration;
|
|
423
|
+
const delay = e.delay;
|
|
424
|
+
const shapeSpid = e.shapeSpid || 0;
|
|
425
|
+
const baseId = 100 + idx * 10;
|
|
426
|
+
|
|
427
|
+
// Motion-path entries use <p:animMotion> instead of <p:animEffect>.
|
|
428
|
+
if (e.kind === 'motion') {
|
|
429
|
+
return `
|
|
430
|
+
<p:par>
|
|
431
|
+
<p:cTn id="${baseId}" presetID="0" presetClass="path" presetSubtype="0" fill="hold" grpId="0" nodeType="${nodeType}">
|
|
432
|
+
<p:stCondLst><p:cond delay="${delay}"/></p:stCondLst>
|
|
433
|
+
<p:childTnLst>
|
|
434
|
+
<p:animMotion origin="layout" path="${escapeXmlAttr(e.motionPath)}" pathEditMode="relative" rAng="0" ptsTypes="">
|
|
435
|
+
<p:cBhvr>
|
|
436
|
+
<p:cTn id="${baseId + 1}" dur="${durMs}" fill="hold"/>
|
|
437
|
+
<p:tgtEl><p:spTgt spid="${shapeSpid}"/></p:tgtEl>
|
|
438
|
+
<p:attrNameLst><p:attrName>ppt_x</p:attrName><p:attrName>ppt_y</p:attrName></p:attrNameLst>
|
|
439
|
+
</p:cBhvr>
|
|
440
|
+
</p:animMotion>
|
|
441
|
+
</p:childTnLst>
|
|
442
|
+
</p:cTn>
|
|
443
|
+
</p:par>`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// When textBuild is set, the shape's text reveals piece-by-piece via
|
|
447
|
+
// <p:iterate type="lt|wd|el"> ... <p:tmAbs val="<paceMs>"/></p:iterate>
|
|
448
|
+
// on the outer <p:cTn>. The same preset-specific animation below then
|
|
449
|
+
// fires once per piece.
|
|
450
|
+
const buildMap = { bychar: 'lt', byword: 'wd', byparagraph: 'el' };
|
|
451
|
+
const iterType = e.textBuild ? buildMap[e.textBuild] : null;
|
|
452
|
+
const paceMs = Number.isFinite(e.textPaceMs) ? e.textPaceMs : 80;
|
|
453
|
+
const iterateXml = iterType
|
|
454
|
+
? `<p:iterate type="${iterType}"><p:tmAbs val="${paceMs}"/></p:iterate>`
|
|
455
|
+
: '';
|
|
456
|
+
|
|
457
|
+
// Preset-specific animation body (see animation-renderers.js). Each
|
|
458
|
+
// renderer knows the right <p:set>/<p:anim>/<p:animEffect> children
|
|
459
|
+
// for its effect — this replaces the previous "opacity 0→1 for
|
|
460
|
+
// everything" stub that made every preset look like fadein.
|
|
461
|
+
const childTimeline = renderPresetChildTimeline(e.type, {
|
|
462
|
+
baseId,
|
|
463
|
+
durMs,
|
|
464
|
+
delayMs: delay,
|
|
465
|
+
spid: shapeSpid,
|
|
466
|
+
direction: e.direction,
|
|
467
|
+
presetClass: e.presetClass,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
return `
|
|
471
|
+
<p:par>
|
|
472
|
+
<p:cTn id="${baseId}" presetID="${e.presetID}" presetClass="${e.presetClass}" presetSubtype="${e.presetSubtype}" fill="hold" grpId="0" nodeType="${nodeType}">
|
|
473
|
+
<p:stCondLst><p:cond delay="${delay}"/></p:stCondLst>
|
|
474
|
+
${iterateXml}
|
|
475
|
+
<p:childTnLst>${childTimeline}
|
|
476
|
+
</p:childTnLst>
|
|
477
|
+
</p:cTn>
|
|
478
|
+
</p:par>`;
|
|
479
|
+
})
|
|
480
|
+
.join('');
|
|
481
|
+
|
|
482
|
+
return `
|
|
483
|
+
<p:timing>
|
|
484
|
+
<p:tnLst>
|
|
485
|
+
<p:par>
|
|
486
|
+
<p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot">
|
|
487
|
+
<p:childTnLst>
|
|
488
|
+
<p:seq concurrent="1" nextAc="seek">
|
|
489
|
+
<p:cTn id="2" dur="indefinite" nodeType="mainSeq">
|
|
490
|
+
<p:childTnLst>${parList}
|
|
491
|
+
</p:childTnLst>
|
|
492
|
+
</p:cTn>
|
|
493
|
+
<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>
|
|
494
|
+
<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>
|
|
495
|
+
</p:seq>
|
|
496
|
+
</p:childTnLst>
|
|
497
|
+
</p:cTn>
|
|
498
|
+
</p:par>
|
|
499
|
+
</p:tnLst>
|
|
500
|
+
</p:timing>`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ----------------------------------------------------------------------
|
|
504
|
+
* injectIntoSlideXml — mutate an existing slideN.xml string
|
|
505
|
+
* Inserts <p:transition> and <p:timing> immediately before </p:cSld>-sibling
|
|
506
|
+
* elements (they are siblings of <p:cSld>, not inside it).
|
|
507
|
+
*
|
|
508
|
+
* Structure expected:
|
|
509
|
+
* <p:sld ...>
|
|
510
|
+
* <p:cSld>...</p:cSld>
|
|
511
|
+
* [<p:clrMapOvr>...</p:clrMapOvr>]
|
|
512
|
+
* [<p:transition>...</p:transition>] ← we insert here
|
|
513
|
+
* [<p:timing>...</p:timing>] ← and here
|
|
514
|
+
* </p:sld>
|
|
515
|
+
* ---------------------------------------------------------------------- */
|
|
516
|
+
export function injectIntoSlideXml(slideXml, { transition, entries }) {
|
|
517
|
+
let xml = slideXml;
|
|
518
|
+
|
|
519
|
+
// Resolve shape ids: walk <p:sp> / <p:pic> elements in order. The Nth one
|
|
520
|
+
// corresponds to shapeIndex === N. Extract the numeric id from <p:cNvPr id="..."/>.
|
|
521
|
+
const shapeRegex = /<p:(sp|pic|grpSp|graphicFrame)\b[^>]*>\s*<p:nvSpPr>\s*<p:cNvPr id="(\d+)"/g;
|
|
522
|
+
const altRegex = /<p:cNvPr id="(\d+)"/g;
|
|
523
|
+
const ids = [];
|
|
524
|
+
let m;
|
|
525
|
+
while ((m = altRegex.exec(xml)) !== null) ids.push(parseInt(m[1], 10));
|
|
526
|
+
// Skip first id (group root) — typical PptxGenJS output starts with an id=1 root group.
|
|
527
|
+
const shapeIds = ids.slice(1);
|
|
528
|
+
|
|
529
|
+
const resolvedEntries = entries
|
|
530
|
+
.map((e) => ({
|
|
531
|
+
...e,
|
|
532
|
+
shapeSpid:
|
|
533
|
+
Number.isFinite(e.shapeIndex) && shapeIds[e.shapeIndex] != null
|
|
534
|
+
? shapeIds[e.shapeIndex]
|
|
535
|
+
: shapeIds[0] || 0,
|
|
536
|
+
}))
|
|
537
|
+
.filter((e) => e.shapeSpid > 0);
|
|
538
|
+
|
|
539
|
+
const transitionXml = buildTransitionXml(transition);
|
|
540
|
+
const timingXml = buildTimingXml(resolvedEntries);
|
|
541
|
+
|
|
542
|
+
const insertion = `${transitionXml}${timingXml}`;
|
|
543
|
+
if (!insertion) return xml;
|
|
544
|
+
|
|
545
|
+
// Remove any existing p:timing/p:transition that PptxGenJS may have put in
|
|
546
|
+
xml = xml.replace(/<p:transition\b[^<]*(?:<[^/][^>]*>[\s\S]*?<\/p:transition>|\/?>)/g, '');
|
|
547
|
+
xml = xml.replace(/<p:timing\b[\s\S]*?<\/p:timing>/g, '');
|
|
548
|
+
|
|
549
|
+
// Insert right before </p:sld>
|
|
550
|
+
xml = xml.replace('</p:sld>', `${insertion}</p:sld>`);
|
|
551
|
+
return xml;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ----------------------------------------------------------------------
|
|
555
|
+
* applyPlansToZip — given a loaded JSZip PPTX and a list of plans, mutate
|
|
556
|
+
* slides/slideN.xml in place.
|
|
557
|
+
* ---------------------------------------------------------------------- */
|
|
558
|
+
export async function applyPlansToZip(zip, plans) {
|
|
559
|
+
for (const plan of plans) {
|
|
560
|
+
const path = `ppt/slides/slide${plan.slideIndex + 1}.xml`;
|
|
561
|
+
const entry = zip.file(path);
|
|
562
|
+
if (!entry) {
|
|
563
|
+
console.warn(`[animation-injector] Slide not found: ${path}`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const xml = await entry.async('string');
|
|
567
|
+
const newXml = injectIntoSlideXml(xml, plan);
|
|
568
|
+
zip.file(path, newXml);
|
|
569
|
+
}
|
|
570
|
+
return zip;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* ----------------------------------------------------------------------
|
|
574
|
+
* preProcessHtml — assigns a sequential data-pptx-shape-index to every
|
|
575
|
+
* element that will become a PPTX shape. Must be called BEFORE the main
|
|
576
|
+
* HTML→PPTX conversion so that exportToPptx picks the same order.
|
|
577
|
+
*
|
|
578
|
+
* The heuristic: any element that has data-anim (we need it animated) AND
|
|
579
|
+
* is one of [text container, image, shape div] gets a shape index equal to
|
|
580
|
+
* its position in the slide's top-down traversal order.
|
|
581
|
+
* ---------------------------------------------------------------------- */
|
|
582
|
+
export function preProcessHtml(rootElement) {
|
|
583
|
+
const slides = rootElement.querySelectorAll('section.slide');
|
|
584
|
+
slides.forEach((slide) => {
|
|
585
|
+
let counter = 0;
|
|
586
|
+
// NOTE: this traversal intentionally matches exportToPptx's own order —
|
|
587
|
+
// depth-first, document order. Elements without bg/text are skipped by
|
|
588
|
+
// the main converter, so we also skip them here.
|
|
589
|
+
const all = slide.querySelectorAll('*');
|
|
590
|
+
all.forEach((el) => {
|
|
591
|
+
// Always index elements that explicitly request an animation.
|
|
592
|
+
const wantsAnim =
|
|
593
|
+
el.hasAttribute('data-anim') || el.hasAttribute('data-anim-motion');
|
|
594
|
+
const style = el.getAttribute('style') || '';
|
|
595
|
+
const hasTextOrBg = /position:|background|border|color|font-size/i.test(style);
|
|
596
|
+
if (wantsAnim || hasTextOrBg) {
|
|
597
|
+
el.setAttribute('data-pptx-shape-index', String(counter));
|
|
598
|
+
counter += 1;
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* ----------------------------------------------------------------------
|
|
605
|
+
* Public helpers
|
|
606
|
+
* ---------------------------------------------------------------------- */
|
|
607
|
+
export const SUPPORTED_TRANSITIONS = Object.keys(TRANSITION_PRESETS);
|
|
608
|
+
export const SUPPORTED_ANIMATIONS = Object.keys(ENTRANCE_PRESETS);
|
|
609
|
+
export const SUPPORTED_MOTION_PATHS = Object.keys(MOTION_PATHS);
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Machine-readable catalogue of every animation/transition the injector
|
|
613
|
+
* supports. Used by the MCP `html2pptx_animation_catalog` tool and the
|
|
614
|
+
* public `GET /api/animations/catalog` endpoint. Designed so an AI agent
|
|
615
|
+
* can pick the right animation without reading the source.
|
|
616
|
+
*/
|
|
617
|
+
export function getAnimationCatalog() {
|
|
618
|
+
const entrance = [];
|
|
619
|
+
const emphasis = [];
|
|
620
|
+
const exit = [];
|
|
621
|
+
Object.entries(ENTRANCE_PRESETS).forEach(([name, preset]) => {
|
|
622
|
+
const directions =
|
|
623
|
+
preset.directional === 'cardinal'
|
|
624
|
+
? Object.keys(CARDINAL_SUBTYPES)
|
|
625
|
+
: preset.directional === 'inout'
|
|
626
|
+
? Object.keys(INOUT_SUBTYPES)
|
|
627
|
+
: [];
|
|
628
|
+
const entry = { name, directions };
|
|
629
|
+
if (preset.presetClass === 'entr') entrance.push(entry);
|
|
630
|
+
else if (preset.presetClass === 'emph') emphasis.push(entry);
|
|
631
|
+
else if (preset.presetClass === 'exit') exit.push(entry);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
version: 3,
|
|
636
|
+
dsl: {
|
|
637
|
+
element: {
|
|
638
|
+
'data-anim': 'Animation name (e.g. "fadein", "flyin", "bounce") — see animations.entrance/emphasis/exit',
|
|
639
|
+
'data-anim-direction': 'Optional direction (cardinal: left/right/up/down [+ corners], or in/out for zoom-style)',
|
|
640
|
+
'data-anim-duration': 'Duration in ms (100–5000, default 500)',
|
|
641
|
+
'data-anim-delay': 'Delay in ms before the animation starts (default 0)',
|
|
642
|
+
'data-anim-trigger': 'When to start: onClick (default) | withPrevious | afterPrevious',
|
|
643
|
+
'data-anim-motion': 'Motion path name (e.g. "line", "arc", "loop") — see motionPaths',
|
|
644
|
+
'data-anim-motion-path': 'OPTIONAL custom SVG-style path override, e.g. "M 0 0 L 0.3 -0.2 E" (overrides data-anim-motion preset)',
|
|
645
|
+
'data-anim-text': 'Apply effect piece-by-piece: "bychar" | "byword" | "byparagraph"',
|
|
646
|
+
'data-anim-text-pace': 'Inter-piece delay in ms for text builds (default 80)',
|
|
647
|
+
},
|
|
648
|
+
slide: {
|
|
649
|
+
'data-transition': 'Slide transition name (e.g. "fade", "push", "morph", "vortex", "cube")',
|
|
650
|
+
'data-transition-direction': 'Optional direction for directional transitions (left/right/up/down or in/out or horz/vert)',
|
|
651
|
+
'data-transition-duration': 'Duration in ms (100–5000, default 800)',
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
transitions: Object.keys(TRANSITION_PRESETS).map((name) => ({
|
|
655
|
+
name,
|
|
656
|
+
directional: ['push', 'wipe', 'cover', 'uncover', 'vortex', 'ferris', 'gallery', 'conveyor', 'prism', 'glitter', 'switch', 'flip', 'cube'].includes(name)
|
|
657
|
+
? 'cardinal'
|
|
658
|
+
: ['split', 'warp', 'window', 'shred', 'box'].includes(name)
|
|
659
|
+
? 'inout'
|
|
660
|
+
: name === 'doors'
|
|
661
|
+
? 'horzvert'
|
|
662
|
+
: false,
|
|
663
|
+
modern: ['morph', 'vortex', 'ferris', 'gallery', 'conveyor', 'flash', 'prism', 'glitter', 'honeycomb', 'warp', 'window', 'orbit', 'shred', 'switch', 'flip', 'cube', 'doors', 'box', 'rotate', 'revealSmoothly'].includes(name),
|
|
664
|
+
})),
|
|
665
|
+
animations: { entrance, emphasis, exit },
|
|
666
|
+
motionPaths: Object.keys(MOTION_PATHS),
|
|
667
|
+
triggers: ['onClick', 'withPrevious', 'afterPrevious'],
|
|
668
|
+
examples: [
|
|
669
|
+
{
|
|
670
|
+
title: 'Fade-in title',
|
|
671
|
+
html: '<h1 data-anim="fadein" data-anim-duration="1000">Hello</h1>',
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
title: 'Fly in from the left after previous',
|
|
675
|
+
html: '<p data-anim="flyin" data-anim-direction="left" data-anim-delay="300" data-anim-trigger="afterPrevious">Body</p>',
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
title: 'Slide with fade transition',
|
|
679
|
+
html: '<section class="slide" data-transition="fade" data-transition-duration="600">...</section>',
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
title: 'Staggered list',
|
|
683
|
+
html: [
|
|
684
|
+
'<ul>',
|
|
685
|
+
' <li data-anim="fadein" data-anim-trigger="withPrevious">A</li>',
|
|
686
|
+
' <li data-anim="fadein" data-anim-trigger="afterPrevious" data-anim-delay="200">B</li>',
|
|
687
|
+
' <li data-anim="fadein" data-anim-trigger="afterPrevious" data-anim-delay="400">C</li>',
|
|
688
|
+
'</ul>',
|
|
689
|
+
].join('\n'),
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
title: 'Motion path — element arcs across the slide',
|
|
693
|
+
html: '<div data-anim-motion="arcup" data-anim-duration="1500">Shape</div>',
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
title: 'Custom motion path (SVG-style)',
|
|
697
|
+
html: '<div data-anim-motion="custom" data-anim-motion-path="M 0 0 L 0.4 -0.2 L 0 0 E">Shape</div>',
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
title: 'Typewriter — reveal letter by letter',
|
|
701
|
+
html: '<h1 data-anim="fadein" data-anim-text="bychar" data-anim-text-pace="60" data-anim-duration="40">Hello, world</h1>',
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
title: 'Word-by-word reveal',
|
|
705
|
+
html: '<p data-anim="fadein" data-anim-text="byword" data-anim-text-pace="180">Three things to know</p>',
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
title: 'Morph transition (PowerPoint 2016+)',
|
|
709
|
+
html: '<section class="slide" data-transition="morph" data-transition-duration="1000">...</section>',
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
title: 'Cube transition',
|
|
713
|
+
html: '<section class="slide" data-transition="cube" data-transition-direction="left">...</section>',
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
notes: [
|
|
717
|
+
'HTML authored without data-anim/data-transition/data-anim-motion attributes renders identically to the current html2pptx output (fully backward compatible).',
|
|
718
|
+
'Modern transitions (morph, vortex, ferris, cube, etc.) require PowerPoint 2016+ and emit p14-namespaced ext markers. Older viewers ignore them gracefully.',
|
|
719
|
+
'Motion paths use a compact SVG-style DSL: M (move), L (line), C (cubic bezier), E (end). Coordinates are 0..1 slide-relative.',
|
|
720
|
+
'Text builds (data-anim-text) emit a <p:iterate> timing so PowerPoint reveals the text piece-by-piece at the given pace.',
|
|
721
|
+
'PowerPoint (Mac, Windows, Web), Keynote, and LibreOffice honour most timings. Google Slides may ignore some effects.',
|
|
722
|
+
],
|
|
723
|
+
};
|
|
724
|
+
}
|