@office-kit/pptx-editor 0.0.0-stage → 0.1.1

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,1400 @@
1
+ //#region src/core/animation-player.ts
2
+ const INSTANT_EFFECTS = ["appear", "disappear"];
3
+ const FILTER_FAMILIES = [
4
+ "blinds",
5
+ "checkerboard",
6
+ "peek",
7
+ "randomBars",
8
+ "shape",
9
+ "split",
10
+ "strips",
11
+ "wedge",
12
+ "wheel",
13
+ "wipe"
14
+ ];
15
+ /** The `presetClass` of an effect the library names says which of the three it is. */
16
+ const PRESET_CLASS_KINDS = {
17
+ entr: "entrance",
18
+ exit: "exit",
19
+ emph: "emphasis"
20
+ };
21
+ const motionOf = (effect) => {
22
+ if (INSTANT_EFFECTS.includes(effect)) return "none";
23
+ const family = effect.replace(/(In|Out)$/, "");
24
+ if (family === "fly" || family === "credits") return "fly";
25
+ if (family === "peek") return "peek";
26
+ return FILTER_FAMILIES.includes(family) ? "filter" : "pose";
27
+ };
28
+ /** Where an entrance starts and an exit ends, by preset family. */
29
+ const AWAY = {
30
+ fade: { opacity: 0 },
31
+ dissolve: { opacity: 0 },
32
+ expand: {
33
+ opacity: 0,
34
+ scale: "0.7 1"
35
+ },
36
+ contract: {
37
+ opacity: 0,
38
+ scale: "0.7 1"
39
+ },
40
+ swivel: {
41
+ opacity: 0,
42
+ scale: "0 1"
43
+ },
44
+ zoom: {
45
+ opacity: 0,
46
+ scale: "0"
47
+ },
48
+ basicZoom: { scale: "0" },
49
+ float: {
50
+ opacity: 0,
51
+ translate: "0% 50%"
52
+ },
53
+ growTurn: {
54
+ opacity: 0,
55
+ scale: "0",
56
+ rotate: "90deg"
57
+ },
58
+ shrinkTurn: {
59
+ opacity: 0,
60
+ scale: "0",
61
+ rotate: "90deg"
62
+ },
63
+ spinner: {
64
+ opacity: 0,
65
+ scale: "0",
66
+ rotate: "-90deg"
67
+ },
68
+ stretch: { scale: "0 1" },
69
+ stretchy: { scale: "0 1" },
70
+ collapse: { scale: "0 1" },
71
+ pinwheel: {
72
+ opacity: 0,
73
+ scale: "0",
74
+ rotate: "720deg"
75
+ },
76
+ spiral: {
77
+ scale: "0",
78
+ translate: "-100% 100%"
79
+ },
80
+ basicSwivel: { scale: "0 1" }
81
+ };
82
+ /** Each property of a pose at the shape's own place. */
83
+ const AT_REST = {
84
+ opacity: 1,
85
+ scale: "1",
86
+ rotate: "0deg",
87
+ translate: "0% 0%"
88
+ };
89
+ const restOf = (away) => Object.fromEntries(Object.keys(away).map((name) => [name, AT_REST[name]]));
90
+ const turned = (from, to) => [{ filter: from }, { filter: to }];
91
+ const flash = (from, to) => [
92
+ { filter: from },
93
+ { filter: to },
94
+ { filter: from }
95
+ ];
96
+ const HUE_TURN = turned("hue-rotate(0deg)", "hue-rotate(180deg)");
97
+ const CONTRAST_FLASH = flash("contrast(1)", "contrast(1.8)");
98
+ /** The frames of each emphasis effect, which starts and ends with the shape as drawn. */
99
+ const EMPHASIS = {
100
+ pulse: [
101
+ {
102
+ offset: 0,
103
+ scale: "1",
104
+ opacity: 1
105
+ },
106
+ {
107
+ offset: .2,
108
+ opacity: .5
109
+ },
110
+ {
111
+ offset: .5,
112
+ scale: "1.05"
113
+ },
114
+ {
115
+ offset: .8,
116
+ opacity: .5
117
+ },
118
+ {
119
+ offset: 1,
120
+ scale: "1",
121
+ opacity: 1
122
+ }
123
+ ],
124
+ teeter: [
125
+ {
126
+ offset: 0,
127
+ rotate: "0deg"
128
+ },
129
+ {
130
+ offset: .1,
131
+ rotate: "2deg"
132
+ },
133
+ {
134
+ offset: .2,
135
+ rotate: "2deg"
136
+ },
137
+ {
138
+ offset: .4,
139
+ rotate: "-2deg"
140
+ },
141
+ {
142
+ offset: .6,
143
+ rotate: "2deg"
144
+ },
145
+ {
146
+ offset: .8,
147
+ rotate: "-2deg"
148
+ },
149
+ {
150
+ offset: 1,
151
+ rotate: "0deg"
152
+ }
153
+ ],
154
+ wave: [
155
+ {
156
+ offset: 0,
157
+ translate: "0% 0%",
158
+ rotate: "0deg",
159
+ easing: "ease-in-out"
160
+ },
161
+ {
162
+ offset: .25,
163
+ rotate: "25deg"
164
+ },
165
+ {
166
+ offset: .5,
167
+ translate: "0% -20%",
168
+ rotate: "0deg",
169
+ easing: "ease-in-out"
170
+ },
171
+ {
172
+ offset: .75,
173
+ rotate: "-25deg"
174
+ },
175
+ {
176
+ offset: 1,
177
+ translate: "0% 0%",
178
+ rotate: "0deg"
179
+ }
180
+ ],
181
+ shimmer: [
182
+ {
183
+ scale: "1",
184
+ translate: "0% 0%",
185
+ rotate: "0deg"
186
+ },
187
+ {
188
+ scale: "0.8 1",
189
+ translate: "10% -10%",
190
+ rotate: "-8deg"
191
+ },
192
+ {
193
+ scale: "1",
194
+ translate: "0% 0%",
195
+ rotate: "0deg"
196
+ }
197
+ ],
198
+ blink: [
199
+ {
200
+ offset: 0,
201
+ opacity: 0,
202
+ easing: "step-end"
203
+ },
204
+ {
205
+ offset: .5,
206
+ opacity: 1
207
+ },
208
+ {
209
+ offset: 1,
210
+ opacity: 1
211
+ }
212
+ ],
213
+ fillColor: HUE_TURN,
214
+ lineColor: HUE_TURN,
215
+ fontColor: HUE_TURN,
216
+ objectColor: HUE_TURN,
217
+ brushColor: HUE_TURN,
218
+ complementaryColor: HUE_TURN,
219
+ complementaryColor2: HUE_TURN,
220
+ contrastingColor: HUE_TURN,
221
+ darken: turned("brightness(1)", "brightness(0.6)"),
222
+ lighten: turned("brightness(1)", "brightness(1.4)"),
223
+ desaturate: turned("saturate(1)", "saturate(0)"),
224
+ colorPulse: flash("brightness(1)", "brightness(1.4)"),
225
+ growWithColor: [{
226
+ scale: "1",
227
+ filter: "hue-rotate(0deg)"
228
+ }, {
229
+ scale: "1.1",
230
+ filter: "hue-rotate(180deg)"
231
+ }],
232
+ boldFlash: [
233
+ {
234
+ offset: 0,
235
+ filter: "contrast(1)",
236
+ easing: "step-end"
237
+ },
238
+ {
239
+ offset: .5,
240
+ filter: "contrast(1.8)",
241
+ easing: "step-end"
242
+ },
243
+ {
244
+ offset: .6,
245
+ filter: "contrast(1)"
246
+ },
247
+ {
248
+ offset: 1,
249
+ filter: "contrast(1)"
250
+ }
251
+ ],
252
+ boldReveal: CONTRAST_FLASH,
253
+ underline: CONTRAST_FLASH
254
+ };
255
+ /**
256
+ * The frames of an emphasis effect whose Effect Options decide how far it
257
+ * goes: Spin's angle and direction, Grow/Shrink's size and axes,
258
+ * Transparency's amount. `null` for every other effect.
259
+ */
260
+ const optionedEmphasis = (step) => {
261
+ switch (step.effect) {
262
+ case "spin": return [{ rotate: "0deg" }, { rotate: `${(step.spinDirection === "counterclockwise" ? -1 : 1) * (step.spinDegrees ?? 360)}deg` }];
263
+ case "growShrink": {
264
+ const size = (step.scalePercent ?? 150) / 100;
265
+ return [{ scale: "1" }, { scale: step.scaleDirection === "horizontal" ? `${size} 1` : step.scaleDirection === "vertical" ? `1 ${size}` : `${size}` }];
266
+ }
267
+ case "transparency": return [{ opacity: 1 }, { opacity: 1 - (step.transparencyPercent ?? 50) / 100 }];
268
+ default: return null;
269
+ }
270
+ };
271
+ /** The time-to-progress curve of a `tmFilter`, read at `t`. */
272
+ const filtered = (points, t) => {
273
+ for (let at = 1; at < points.length; at += 1) {
274
+ const [t1, p1] = points[at];
275
+ const [t0, p0] = points[at - 1];
276
+ if (t <= t1) return p0 + (p1 - p0) * (t - t0) / (t1 - t0);
277
+ }
278
+ return 1;
279
+ };
280
+ const BOUNCE_DRIFT = [
281
+ [0, 0],
282
+ [.14, .36],
283
+ [.43, .73],
284
+ [.71, .91],
285
+ [1, 1]
286
+ ];
287
+ const BOUNCE_LENGTH = 1822;
288
+ const BOUNCE_LANDINGS = [
289
+ 664,
290
+ 1324,
291
+ 1656,
292
+ 1822
293
+ ];
294
+ const BOUNCE_HEIGHTS = [
295
+ 1 / 9,
296
+ 1 / 27,
297
+ 1 / 81
298
+ ];
299
+ const REVOLVE_ARC = [
300
+ [
301
+ 0,
302
+ 0,
303
+ .31
304
+ ],
305
+ [
306
+ .25,
307
+ .1096,
308
+ .2646
309
+ ],
310
+ [
311
+ .5,
312
+ .155,
313
+ .155
314
+ ],
315
+ [
316
+ .75,
317
+ .1096,
318
+ .0454
319
+ ],
320
+ [
321
+ 1,
322
+ 0,
323
+ 0
324
+ ]
325
+ ];
326
+ const CURVE_POINTS = [
327
+ [-.46736, .92887],
328
+ [.0908, .66613],
329
+ [.23177, .40825],
330
+ [.18264, .09152],
331
+ [0, 0]
332
+ ];
333
+ const frame = (offset, translate, rest = {}) => translate === null ? null : {
334
+ offset,
335
+ translate,
336
+ ...rest
337
+ };
338
+ const all = (frames) => frames.every((f) => f !== null) ? frames : null;
339
+ /**
340
+ * The entrance presets made of several behaviours, as keyframes from where the
341
+ * shape starts to its own place. Each offset is where one of the reference desktop app's
342
+ * behaviours starts or ends, as a fraction of the preset's default length.
343
+ */
344
+ const TRACKS = {
345
+ bounce: (g) => {
346
+ const at = (ms, height, easing) => frame(ms / BOUNCE_LENGTH, g.shift(-.25 * (1 - filtered(BOUNCE_DRIFT, ms / BOUNCE_LENGTH)), -height), { easing });
347
+ const frames = [at(0, 1 / 3, "ease-in")];
348
+ BOUNCE_LANDINGS.forEach((landing, i) => {
349
+ frames.push(at(landing, 0, "ease-out"));
350
+ const next = BOUNCE_LANDINGS[i + 1];
351
+ if (next !== void 0) frames.push(at((landing + next) / 2, BOUNCE_HEIGHTS[i], "ease-in"));
352
+ });
353
+ return all(frames);
354
+ },
355
+ boomerang: (g) => all([
356
+ frame(0, g.shift(.4, -.2), {
357
+ rotate: "-90deg",
358
+ scale: "1 1",
359
+ opacity: 0,
360
+ easing: "ease-out"
361
+ }),
362
+ frame(.5, g.shift(0, .1), {
363
+ rotate: "0deg",
364
+ scale: "0.05 1",
365
+ opacity: .5,
366
+ easing: "ease-in"
367
+ }),
368
+ frame(1, g.shift(0, 0), {
369
+ rotate: "0deg",
370
+ scale: "1 1",
371
+ opacity: 1
372
+ })
373
+ ]),
374
+ centerRevolve: (g) => all([
375
+ frame(0, g.shift(0, .31), { opacity: 0 }),
376
+ frame(.1, g.shift(0, .31), { opacity: 1 }),
377
+ ...REVOLVE_ARC.map(([t, x, y]) => frame(.4 + .6 * t, g.shift(x, y), { opacity: 1 }))
378
+ ]),
379
+ riseUp: (g) => all([
380
+ frame(0, g.shift(0, 1), {
381
+ opacity: 0,
382
+ easing: "ease-out"
383
+ }),
384
+ frame(.9, g.shift(0, -.03), {
385
+ opacity: .9,
386
+ easing: "ease-in"
387
+ }),
388
+ frame(1, g.shift(0, 0), { opacity: 1 })
389
+ ]),
390
+ floating: (g) => all([
391
+ frame(0, g.shift(.4, -.4), {
392
+ rotate: "-90deg",
393
+ opacity: 0,
394
+ easing: "ease-out"
395
+ }),
396
+ frame(.8, g.shift(-.05, .1), {
397
+ rotate: "0deg",
398
+ opacity: 1,
399
+ easing: "ease-in"
400
+ }),
401
+ frame(1, g.shift(0, 0), {
402
+ rotate: "0deg",
403
+ opacity: 1
404
+ })
405
+ ]),
406
+ whip: (g) => all([
407
+ frame(0, g.shift(0, 0), {
408
+ scale: "0.1",
409
+ opacity: 0
410
+ }),
411
+ frame(.5, g.shift(.1, 0), {
412
+ scale: "1.02",
413
+ opacity: 1
414
+ }),
415
+ frame(1, g.shift(0, 0), {
416
+ scale: "1",
417
+ opacity: 1
418
+ })
419
+ ]),
420
+ curveUp: (g) => all(CURVE_POINTS.map(([x, y], i) => frame(i / (CURVE_POINTS.length - 1), g.shift(x, y), {
421
+ scale: `${2.5 - 1.5 * i / (CURVE_POINTS.length - 1)}`,
422
+ opacity: i / (CURVE_POINTS.length - 1)
423
+ }))),
424
+ drop: (g) => all([
425
+ frame(0, g.shift(0, -1), {
426
+ rotate: "-45deg",
427
+ easing: "ease-in"
428
+ }),
429
+ frame(.455, g.shift(0, 0, 0, -10), {
430
+ rotate: "-45deg",
431
+ easing: "ease-out"
432
+ }),
433
+ frame(.611, g.shift(0, 0, 0, -50), {
434
+ rotate: "0deg",
435
+ easing: "ease-in"
436
+ }),
437
+ frame(.767, g.shift(0, 0, 0, -10), { rotate: "42deg" }),
438
+ frame(.864, g.shift(0, 0, 0, -10), { rotate: "15deg" }),
439
+ frame(.91, g.shift(0, 0, 0, -5), { rotate: "0deg" }),
440
+ frame(1, g.shift(0, 0), { rotate: "0deg" })
441
+ ]),
442
+ flip: (g) => flipTrack(g, "top")
443
+ };
444
+ const flipTrack = (g, edge) => {
445
+ const past = g.past(edge);
446
+ if (past === null) return null;
447
+ const at = (share, own) => `calc(${past.dx * share}px + ${own}%) calc(${past.dy * share}px + 0%)`;
448
+ const entering = edge === "top";
449
+ const from = entering ? 1 : 0;
450
+ const to = entering ? 0 : 1;
451
+ return [
452
+ {
453
+ offset: 0,
454
+ translate: at(from, 0),
455
+ scale: "1 1",
456
+ rotate: "0deg"
457
+ },
458
+ {
459
+ offset: .5,
460
+ translate: at(.5, 50),
461
+ scale: "-1 1",
462
+ rotate: "180deg"
463
+ },
464
+ {
465
+ offset: 1,
466
+ translate: at(to, 0),
467
+ scale: "1 1",
468
+ rotate: "360deg"
469
+ }
470
+ ];
471
+ };
472
+ /** The exits whose track is not their entrance's, played backwards. */
473
+ const EXIT_TRACKS = {
474
+ drop: (g) => all([frame(0, g.shift(0, 0), {
475
+ rotate: "0deg",
476
+ easing: "ease-in"
477
+ }), frame(1, g.shift(0, 1), { rotate: "45deg" })]),
478
+ flip: (g) => flipTrack(g, "bottom")
479
+ };
480
+ /** The entrance track whose reverse is an exit's: Sink Down is Rise Up backwards. */
481
+ const TRACK_OF_EXIT = {
482
+ sinkDown: "riseUp",
483
+ curveDown: "curveUp"
484
+ };
485
+ const SWAPPED_EASING = {
486
+ "ease-in": "ease-out",
487
+ "ease-out": "ease-in"
488
+ };
489
+ /**
490
+ * A track played backwards: the last frame first, each offset mirrored, and
491
+ * the easing of each stretch moved to the frame that now starts it, turned
492
+ * round — a stretch that slowed into a frame speeds out of it.
493
+ */
494
+ const reversed = (frames) => {
495
+ const count = frames.length;
496
+ return frames.map((_, i) => {
497
+ const pose = { ...frames[count - 1 - i] };
498
+ delete pose.easing;
499
+ if (typeof pose.offset === "number") pose.offset = 1 - pose.offset;
500
+ const easing = frames[count - 2 - i]?.easing;
501
+ if (typeof easing === "string") pose.easing = SWAPPED_EASING[easing] ?? easing;
502
+ return pose;
503
+ });
504
+ };
505
+ /** The track of a preset, in the direction the step plays it; `undefined` when it has none. */
506
+ const trackOf = (item) => {
507
+ const family = item.step.effect?.replace(/(In|Out)$/, "");
508
+ if (family === void 0) return void 0;
509
+ if (item.kind === "entrance") return TRACKS[family];
510
+ const own = EXIT_TRACKS[family];
511
+ if (own !== void 0) return own;
512
+ const entrance = TRACKS[TRACK_OF_EXIT[family] ?? family];
513
+ return entrance === void 0 ? void 0 : (g) => {
514
+ const frames = entrance(g);
515
+ return frames === null ? null : reversed(frames);
516
+ };
517
+ };
518
+ const styled = (el) => el instanceof HTMLElement || el instanceof SVGElement ? el : null;
519
+ /** Whether a step reveals its target, hides it, or neither; null when we cannot tell. */
520
+ const kindOf = (step) => {
521
+ if (step.effect === null || step.presetClass === null) return null;
522
+ return PRESET_CLASS_KINDS[step.presetClass] ?? null;
523
+ };
524
+ /**
525
+ * How long an effect runs, or null when the deck does not say. Null is not
526
+ * zero: it is the reason nothing can be chained onto the end of this effect.
527
+ */
528
+ const durationOf = (step) => {
529
+ if (step.durationMs !== null && Number.isFinite(step.durationMs) && step.durationMs >= 0) return step.durationMs;
530
+ if (step.effect !== null && INSTANT_EFFECTS.includes(step.effect)) return 0;
531
+ return null;
532
+ };
533
+ /**
534
+ * Groups the steps into the stops the viewer clicks through, with each step's
535
+ * start measured from the beginning of its stop, and lists the steps that are
536
+ * left out with the reason why.
537
+ */
538
+ const buildAnimationStops = (steps) => {
539
+ const stops = [];
540
+ const unsupported = [];
541
+ const skip = (step, reason) => {
542
+ unsupported.push({
543
+ step,
544
+ reason
545
+ });
546
+ };
547
+ let stop = null;
548
+ let groupStart = 0;
549
+ let groupEnd = 0;
550
+ let seen = false;
551
+ for (const step of steps ?? []) {
552
+ if (step.sequence !== "mainSeq") {
553
+ skip(step, "notInMainSeq");
554
+ continue;
555
+ }
556
+ const leading = !seen;
557
+ seen = true;
558
+ if (step.start === "unknown") {
559
+ groupStart = null;
560
+ groupEnd = null;
561
+ skip(step, "notModelled");
562
+ continue;
563
+ }
564
+ if (step.start === "click" || leading) {
565
+ stop = {
566
+ items: [],
567
+ auto: leading && step.start !== "click"
568
+ };
569
+ stops.push(stop);
570
+ groupStart = 0;
571
+ groupEnd = 0;
572
+ } else if (step.start === "afterPrevious") {
573
+ groupStart = groupEnd;
574
+ groupEnd = groupStart;
575
+ }
576
+ const kind = step.playable ? kindOf(step) : null;
577
+ const duration = kind === null ? null : durationOf(step);
578
+ const delay = step.delayMs;
579
+ if (kind === null || delay === null || groupStart === null) {
580
+ skip(step, kind === null || delay === null ? "notModelled" : "unknownTiming");
581
+ groupEnd = null;
582
+ continue;
583
+ }
584
+ const begin = groupStart + delay;
585
+ stop?.items.push({
586
+ step,
587
+ kind,
588
+ begin,
589
+ duration
590
+ });
591
+ groupEnd = duration === null || groupEnd === null ? null : Math.max(groupEnd, begin + duration);
592
+ }
593
+ return {
594
+ stops,
595
+ unsupported
596
+ };
597
+ };
598
+ const shapeSelector = (id) => `[data-pptx-shape-id="${id}"]`;
599
+ /**
600
+ * The elements one step animates: a whole object, or paragraphs of its text.
601
+ * Empty when the step's target is one we do not act on.
602
+ */
603
+ const targetsOf = (root, step, blocked) => {
604
+ const target = step.target;
605
+ if (target.kind !== "shape" && target.kind !== "paragraphs") return [];
606
+ if (!Number.isInteger(target.shapeId)) return [];
607
+ const id = String(target.shapeId);
608
+ if (blocked.has(id)) return [];
609
+ const shapes = root.querySelectorAll(shapeSelector(id));
610
+ const shape = shapes.length === 1 ? styled(shapes[0]) : null;
611
+ if (shape === null) return [];
612
+ if (target.kind !== "paragraphs") return [shape];
613
+ const out = [];
614
+ for (const marker of shape.querySelectorAll("[data-pptx-paragraph]")) {
615
+ if (marker.closest("[data-pptx-shape-id]") !== shape) continue;
616
+ const n = Number(marker.getAttribute("data-pptx-paragraph"));
617
+ if (n < target.firstParagraph || n > target.lastParagraph) continue;
618
+ const element = styled(marker);
619
+ if (element !== null) out.push(element);
620
+ }
621
+ return out;
622
+ };
623
+ const nowMs = () => typeof performance === "object" ? performance.now() : Date.now();
624
+ /**
625
+ * The slide's own box on screen. A fly is stated against the slide's edges
626
+ * (`1+#ppt_h/2` puts the shape's centre one half-height below the bottom of it),
627
+ * so that is what the distance has to be measured from — never the viewport,
628
+ * which is not where the slide ends.
629
+ */
630
+ const slideBoxOf = (el, root) => {
631
+ const svg = el.closest("svg");
632
+ if (svg !== null) return svg.getBoundingClientRect();
633
+ return root instanceof Element ? root.getBoundingClientRect() : null;
634
+ };
635
+ /**
636
+ * The matrix taking the element's own coordinates to the screen, which is also
637
+ * the space a CSS transform written on it is read in.
638
+ *
639
+ * The shape and paragraph markers carry no transform of their own — the
640
+ * renderer wraps the drawn shape rather than transforming the wrapper — so the
641
+ * matrix does not have to be un-done first, and a shape the deck rotates or a
642
+ * group it scales is accounted for by the ancestors it comes through. Text laid
643
+ * out as HTML sits in a `<foreignObject>`, whose own matrix is the one its
644
+ * pixels are scaled by.
645
+ */
646
+ const screenMatrixOf = (el) => {
647
+ if (el instanceof SVGGraphicsElement) return el.getScreenCTM();
648
+ const host = el.closest("foreignObject");
649
+ return host instanceof SVGGraphicsElement ? host.getScreenCTM() : null;
650
+ };
651
+ /**
652
+ * How far the element has to move to sit just outside the named edge, in the
653
+ * units its own CSS transform is written in. `null` when the slide has not been
654
+ * laid out yet, or its matrix cannot be inverted.
655
+ */
656
+ const flyOffset = (el, direction, root) => {
657
+ const slide = slideBoxOf(el, root);
658
+ if (slide === null || slide.width <= 0 || slide.height <= 0) return null;
659
+ const box = el.getBoundingClientRect();
660
+ const lower = direction.toLowerCase();
661
+ return toOwnUnits(el, lower.includes("right") ? slide.right - box.left : lower.includes("left") ? slide.left - box.right : 0, lower.includes("bottom") ? slide.bottom - box.top : lower.includes("top") ? slide.top - box.bottom : 0);
662
+ };
663
+ /** A distance on screen in the units the element's own CSS transform is written in. */
664
+ const toOwnUnits = (el, dx, dy) => {
665
+ const matrix = screenMatrixOf(el);
666
+ if (matrix === null) return {
667
+ dx,
668
+ dy
669
+ };
670
+ const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
671
+ if (determinant === 0) return null;
672
+ return {
673
+ dx: (matrix.d * dx - matrix.c * dy) / determinant,
674
+ dy: (matrix.a * dy - matrix.b * dx) / determinant
675
+ };
676
+ };
677
+ const geometryOf = (el, root) => ({
678
+ shift(slideX, slideY, ownX = 0, ownY = 0) {
679
+ const slide = slideBoxOf(el, root);
680
+ if (slide === null || slide.width <= 0 || slide.height <= 0) return null;
681
+ const own = toOwnUnits(el, slideX * slide.width, slideY * slide.height);
682
+ if (own === null) return null;
683
+ return `calc(${own.dx}px + ${ownX}%) calc(${own.dy}px + ${ownY}%)`;
684
+ },
685
+ past(edge) {
686
+ return flyOffset(el, edge, root);
687
+ }
688
+ });
689
+ /**
690
+ * Each motion animates a property of its own — `translate`, `scale`, `rotate`
691
+ * — rather than all of them sharing `transform`.
692
+ *
693
+ * Two reasons, both of them things the deck states and a single `transform`
694
+ * would lose. The first is that PresentationML animates a shape's position, its
695
+ * size and its rotation as separate attributes (`ppt_x`/`ppt_y`, `ppt_w`/
696
+ * `ppt_h`, `r`), so a slide may well run a fly and a spin over one shape at the
697
+ * same moment; written to one property, the animation that begins second
698
+ * replaces the first outright and the shape would only turn. The second is that
699
+ * the drawn slide uses `transform` itself, for the rotation and the group
700
+ * scaling the deck gives a shape — and the individual properties apply on top
701
+ * of it rather than in place of it, so nothing the renderer did is overwritten.
702
+ */
703
+ /**
704
+ * Whether a step's motion has to be measured against the drawn slide, so that a
705
+ * movement of `null` means "not read yet" rather than "nothing to run".
706
+ */
707
+ const needsGeometry = (item) => item.step.effect !== null && (motionOf(item.step.effect) === "fly" || trackOf(item) !== void 0);
708
+ /**
709
+ * The keyframes one step runs on one element, or `null` when there is nothing
710
+ * to run: an instant effect, or a fly whose distance cannot be measured. Either
711
+ * way the element is put where the step leaves it without motion, which for
712
+ * every effect modelled here is the element's own untransformed place.
713
+ */
714
+ const movementFor = (item, el, root) => {
715
+ const effect = item.step.effect;
716
+ const entering = item.kind === "entrance";
717
+ if (effect === null) return null;
718
+ switch (motionOf(effect)) {
719
+ case "none": return null;
720
+ case "pose": {
721
+ if (item.kind === "emphasis") {
722
+ const frames = optionedEmphasis(item.step) ?? EMPHASIS[effect];
723
+ return frames === void 0 ? null : {
724
+ frames,
725
+ aboutOwnCentre: true
726
+ };
727
+ }
728
+ const track = trackOf(item);
729
+ if (track !== void 0) {
730
+ const frames = track(geometryOf(el, root));
731
+ return frames === null ? null : {
732
+ frames,
733
+ aboutOwnCentre: true
734
+ };
735
+ }
736
+ const away = AWAY[effect.replace(/(In|Out)$/, "")];
737
+ if (away === void 0) return null;
738
+ const rest = restOf(away);
739
+ return {
740
+ frames: entering ? [away, rest] : [rest, away],
741
+ aboutOwnCentre: true
742
+ };
743
+ }
744
+ case "fly": {
745
+ const direction = effect === "creditsIn" ? "bottom" : effect === "creditsOut" ? "top" : item.step.direction;
746
+ if (direction === null) return null;
747
+ const offset = flyOffset(el, direction, root);
748
+ if (offset === null) return null;
749
+ const away = { translate: `${offset.dx}px ${offset.dy}px` };
750
+ const place = { translate: "0px 0px" };
751
+ return {
752
+ frames: entering ? [away, place] : [place, away],
753
+ aboutOwnCentre: false
754
+ };
755
+ }
756
+ case "filter":
757
+ case "peek": {
758
+ const reveal = filterReveal(item.step);
759
+ if (reveal === null) return null;
760
+ const covered = { clipPath: reveal.covered };
761
+ const uncovered = { clipPath: reveal.uncovered };
762
+ const shift = item.step.direction === null ? void 0 : EDGE_VECTORS[item.step.direction];
763
+ if (effect === "peekIn" || effect === "peekOut") {
764
+ if (shift === void 0) return null;
765
+ covered.translate = `${shift[0] * 112.5}% ${shift[1] * 112.5}%`;
766
+ uncovered.translate = "0% 0%";
767
+ }
768
+ return {
769
+ frames: entering ? [covered, uncovered] : [uncovered, covered],
770
+ aboutOwnCentre: true
771
+ };
772
+ }
773
+ }
774
+ };
775
+ /** Which way each edge lies from the centre, as unit steps in x and y. */
776
+ const EDGE_VECTORS = {
777
+ top: [0, -1],
778
+ bottom: [0, 1],
779
+ left: [-1, 0],
780
+ right: [1, 0]
781
+ };
782
+ const OPPOSITE_EDGE = {
783
+ top: "bottom",
784
+ bottom: "top",
785
+ left: "right",
786
+ right: "left"
787
+ };
788
+ /** `inset()` with nothing showing but the named edge's zero-width strip. */
789
+ const COVERED_FROM_EDGE = {
790
+ top: "inset(0% 0% 100% 0%)",
791
+ bottom: "inset(100% 0% 0% 0%)",
792
+ left: "inset(0% 100% 0% 0%)",
793
+ right: "inset(0% 0% 0% 100%)"
794
+ };
795
+ const CORNERS = {
796
+ topLeft: [0, 0],
797
+ topRight: [100, 0],
798
+ bottomLeft: [0, 100],
799
+ bottomRight: [100, 100]
800
+ };
801
+ const polygon = (points, evenOdd = false) => `polygon(${evenOdd ? "evenodd, " : ""}${points.map(([x, y]) => `${x}% ${y}%`).join(", ")}) fill-box`;
802
+ const FULL_BOX = [
803
+ [0, 0],
804
+ [100, 0],
805
+ [100, 100],
806
+ [0, 100]
807
+ ];
808
+ /**
809
+ * `count` bars across the box, each `fraction` of its slot wide. The bars are
810
+ * one polygon, joined along the box's edge by lines of no width.
811
+ */
812
+ const bars = (count, horizontal, fraction) => {
813
+ const points = [];
814
+ for (let i = 0; i < count; i++) {
815
+ const from = 100 * i / count;
816
+ const to = from + 100 * fraction / count;
817
+ if (horizontal) points.push([0, from], [100, from], [100, to], [0, to]);
818
+ else points.push([from, 0], [from, 100], [to, 100], [to, 0]);
819
+ }
820
+ return polygon(points);
821
+ };
822
+ const CIRCLE_RADIUS = 50 * Math.SQRT2;
823
+ const CIRCLE_POINTS = 32;
824
+ const FAN_POINTS = 12;
825
+ /** An outline about the box's centre, `scale` 1 being large enough to cover the box. */
826
+ const outline = (shape, scale) => {
827
+ const at = (x, y) => [50 + x * scale, 50 + y * scale];
828
+ switch (shape) {
829
+ case "box": return [
830
+ at(-50, -50),
831
+ at(50, -50),
832
+ at(50, 50),
833
+ at(-50, 50)
834
+ ];
835
+ case "diamond": return [
836
+ at(0, -100),
837
+ at(100, 0),
838
+ at(0, 100),
839
+ at(-100, 0)
840
+ ];
841
+ case "plus": return [
842
+ at(-50, -100),
843
+ at(50, -100),
844
+ at(50, -50),
845
+ at(100, -50),
846
+ at(100, 50),
847
+ at(50, 50),
848
+ at(50, 100),
849
+ at(-50, 100),
850
+ at(-50, 50),
851
+ at(-100, 50),
852
+ at(-100, -50),
853
+ at(-50, -50)
854
+ ];
855
+ default: return Array.from({ length: CIRCLE_POINTS }, (_, i) => {
856
+ const angle = 2 * Math.PI * i / CIRCLE_POINTS;
857
+ return at(CIRCLE_RADIUS * Math.cos(angle), CIRCLE_RADIUS * Math.sin(angle));
858
+ });
859
+ }
860
+ };
861
+ /**
862
+ * A fan from the centre, swept `sweep` of a turn from `start` — both in turns,
863
+ * clockwise from 12 o'clock — and long enough to reach every corner.
864
+ */
865
+ const fan = (start, sweep) => [[50, 50], ...Array.from({ length: 13 }, (_, i) => {
866
+ const angle = 2 * Math.PI * (start + sweep * i / FAN_POINTS);
867
+ return [50 + 100 * Math.sin(angle), 50 - 100 * Math.cos(angle)];
868
+ })];
869
+ const filterReveal = (step) => {
870
+ const effect = step.effect;
871
+ if (effect === null) return null;
872
+ const family = effect.replace(/(In|Out)$/, "");
873
+ const horizontal = step.orientation !== "vertical";
874
+ switch (family) {
875
+ case "wipe":
876
+ case "peek": {
877
+ const edge = step.direction === null ? void 0 : family === "peek" ? OPPOSITE_EDGE[step.direction] : step.direction;
878
+ const covered = edge === void 0 ? void 0 : COVERED_FROM_EDGE[edge];
879
+ if (covered === void 0) return null;
880
+ return {
881
+ covered: `${covered} fill-box`,
882
+ uncovered: "inset(0% 0% 0% 0%) fill-box"
883
+ };
884
+ }
885
+ case "blinds": return {
886
+ covered: bars(6, horizontal, 0),
887
+ uncovered: bars(6, horizontal, 1)
888
+ };
889
+ case "randomBars": return {
890
+ covered: bars(16, horizontal, 0),
891
+ uncovered: bars(16, horizontal, 1)
892
+ };
893
+ case "checkerboard": return {
894
+ covered: bars(8, !horizontal, 0),
895
+ uncovered: bars(8, !horizontal, 1)
896
+ };
897
+ case "split": {
898
+ const vertical = step.orientation === "vertical";
899
+ if (step.inOut === "out") {
900
+ const line = (open) => {
901
+ const side = `${50 - open / 2}%`;
902
+ return vertical ? `inset(0% ${side} 0% ${side}) fill-box` : `inset(${side} 0% ${side} 0%) fill-box`;
903
+ };
904
+ return {
905
+ covered: line(0),
906
+ uncovered: line(100)
907
+ };
908
+ }
909
+ const edges = (p) => polygon(vertical ? [
910
+ [0, 0],
911
+ [p, 0],
912
+ [p, 100],
913
+ [100 - p, 100],
914
+ [100 - p, 0],
915
+ [100, 0],
916
+ [100, 100],
917
+ [0, 100]
918
+ ] : [
919
+ [0, 0],
920
+ [100, 0],
921
+ [100, p],
922
+ [0, p],
923
+ [0, 100 - p],
924
+ [100, 100 - p],
925
+ [100, 100],
926
+ [0, 100]
927
+ ]);
928
+ return {
929
+ covered: edges(0),
930
+ uncovered: edges(50)
931
+ };
932
+ }
933
+ case "shape": {
934
+ const shape = step.shape ?? "circle";
935
+ if (step.inOut === "out") return {
936
+ covered: polygon(outline(shape, 0)),
937
+ uncovered: polygon(outline(shape, 1))
938
+ };
939
+ return {
940
+ covered: polygon([...FULL_BOX, ...outline(shape, 1)], true),
941
+ uncovered: polygon([...FULL_BOX, ...outline(shape, 0)], true)
942
+ };
943
+ }
944
+ case "strips": {
945
+ const corner = step.direction === null ? void 0 : CORNERS[step.direction];
946
+ if (corner === void 0) return null;
947
+ const [cx, cy] = corner;
948
+ const sx = cx === 0 ? 1 : -1;
949
+ const sy = cy === 0 ? 1 : -1;
950
+ const triangle = (size) => polygon([
951
+ [cx, cy],
952
+ [cx + sx * size, cy],
953
+ [cx, cy + sy * size]
954
+ ]);
955
+ return {
956
+ covered: triangle(0),
957
+ uncovered: triangle(200)
958
+ };
959
+ }
960
+ case "wedge": return {
961
+ covered: polygon([...fan(0, 0), ...fan(0, 0)]),
962
+ uncovered: polygon([...fan(0, .5), ...fan(0, -.5)])
963
+ };
964
+ case "wheel": {
965
+ const spokes = step.spokes ?? 1;
966
+ const sectors = (sweep) => polygon(Array.from({ length: spokes }, (_, i) => fan(i / spokes, sweep / spokes)).flat());
967
+ return {
968
+ covered: sectors(0),
969
+ uncovered: sectors(1)
970
+ };
971
+ }
972
+ default: return null;
973
+ }
974
+ };
975
+ /** Plays a slide's animations against a rendered SVG. */
976
+ const createAnimationPlayer = (options) => {
977
+ const root = () => typeof options.root === "function" ? options.root() : options.root;
978
+ const parsed = buildAnimationStops(options.steps);
979
+ const stops = parsed.stops;
980
+ const unsupported = [...parsed.unsupported];
981
+ const reduced = () => options.reducedMotion?.() === true;
982
+ const onChange = options.onChange ?? (() => {});
983
+ const animations = [];
984
+ const timers = [];
985
+ const touched = /* @__PURE__ */ new Set();
986
+ const inFlight = /* @__PURE__ */ new Map();
987
+ const frozen = /* @__PURE__ */ new Map();
988
+ const outstanding = /* @__PURE__ */ new Map();
989
+ const finished = /* @__PURE__ */ new Set();
990
+ let cursor = 0;
991
+ let disposed = false;
992
+ let startedAt = null;
993
+ let generation = 0;
994
+ const blocked = /* @__PURE__ */ new Set();
995
+ for (const entry of unsupported) for (const shapeId of entry.step.targetShapeIds) blocked.add(String(shapeId));
996
+ const drawn = /* @__PURE__ */ new Map();
997
+ for (const stop of stops) for (const item of stop.items) {
998
+ const id = String(item.step.target.shapeId);
999
+ if (!drawn.has(id)) drawn.set(id, root().querySelectorAll(shapeSelector(id)).length);
1000
+ }
1001
+ for (const stop of stops) for (const item of stop.items) if ((drawn.get(String(item.step.target.shapeId)) ?? 0) > 1) unsupported.push({
1002
+ step: item.step,
1003
+ reason: "ambiguousTarget"
1004
+ });
1005
+ for (const [id, count] of drawn) if (count > 1) blocked.add(id);
1006
+ /**
1007
+ * Where two effects animate one element at the same time, what the slide
1008
+ * shows when the first of them ends turns on whether its value stays above
1009
+ * the other or gives way to it. `valueAfterEnd` says which; where the tree
1010
+ * does not say, there is nothing to play, so the step is reported and what it
1011
+ * names is left as the renderer drew it. On its own such a step is no
1012
+ * question at all — nothing is underneath it, and its own visibility decides
1013
+ * where the shape is left — so it plays as usual.
1014
+ *
1015
+ * Read per element, which is where "the same thing" is actually settled: a
1016
+ * build's paragraphs are separate elements. Each element's effects are sorted
1017
+ * once and swept twice, for what begins before an earlier one has ended and
1018
+ * what is still running when a later one begins — the pairs themselves are
1019
+ * never enumerated.
1020
+ *
1021
+ * Run again whenever the slide is drawn again, because which elements a step
1022
+ * reaches is a fact about the drawing: a transition hands over a layer, and a
1023
+ * re-render replaces the nodes underneath us.
1024
+ */
1025
+ const silence = (current) => {
1026
+ const meets = /* @__PURE__ */ new Set();
1027
+ for (const stop of stops) {
1028
+ const spans = /* @__PURE__ */ new Map();
1029
+ for (const item of stop.items) {
1030
+ const end = item.duration === null ? Number.POSITIVE_INFINITY : item.begin + item.duration;
1031
+ for (const el of targetsOf(current, item.step, blocked)) {
1032
+ const span = {
1033
+ item,
1034
+ begin: item.begin,
1035
+ end
1036
+ };
1037
+ const list = spans.get(el);
1038
+ if (list === void 0) spans.set(el, [span]);
1039
+ else list.push(span);
1040
+ }
1041
+ }
1042
+ for (const list of spans.values()) {
1043
+ if (list.length < 2) continue;
1044
+ list.sort((a, b) => a.begin - b.begin);
1045
+ let furthest = Number.NEGATIVE_INFINITY;
1046
+ for (const span of list) {
1047
+ if (span.begin < furthest) meets.add(span.item);
1048
+ furthest = Math.max(furthest, span.end);
1049
+ }
1050
+ let earliest = Number.POSITIVE_INFINITY;
1051
+ for (let at = list.length - 1; at >= 0; at -= 1) {
1052
+ const span = list[at];
1053
+ if (span === void 0) continue;
1054
+ if (span.end > earliest) meets.add(span.item);
1055
+ earliest = Math.min(earliest, span.begin);
1056
+ }
1057
+ }
1058
+ }
1059
+ for (const stop of stops) {
1060
+ let kept = 0;
1061
+ for (const item of stop.items) {
1062
+ if (item.step.valueAfterEnd === "unstated" && meets.has(item)) {
1063
+ unsupported.push({
1064
+ step: item.step,
1065
+ reason: "notModelled"
1066
+ });
1067
+ for (const shapeId of item.step.targetShapeIds) blocked.add(String(shapeId));
1068
+ continue;
1069
+ }
1070
+ stop.items[kept] = item;
1071
+ kept += 1;
1072
+ }
1073
+ stop.items.length = kept;
1074
+ }
1075
+ };
1076
+ const clearPending = () => {
1077
+ for (const timer of timers) clearTimeout(timer);
1078
+ timers.length = 0;
1079
+ for (const animation of animations) animation.cancel();
1080
+ animations.length = 0;
1081
+ for (const list of frozen.values()) for (const animation of list) animation.cancel();
1082
+ frozen.clear();
1083
+ inFlight.clear();
1084
+ outstanding.clear();
1085
+ finished.clear();
1086
+ };
1087
+ const drop = (list, entry) => {
1088
+ const at = list.indexOf(entry);
1089
+ if (at >= 0) list.splice(at, 1);
1090
+ };
1091
+ const notifyChanged = () => {
1092
+ if (disposed) return;
1093
+ if (timers.length === 0 && animations.length === 0) startedAt = null;
1094
+ onChange(player);
1095
+ };
1096
+ const centreOn = (el) => {
1097
+ touched.add(el);
1098
+ el.style.transformBox = "fill-box";
1099
+ el.style.transformOrigin = "50% 50%";
1100
+ };
1101
+ const uncentre = (el) => {
1102
+ el.style.transformBox = "";
1103
+ el.style.transformOrigin = "";
1104
+ };
1105
+ const show = (el) => {
1106
+ touched.add(el);
1107
+ el.style.visibility = "";
1108
+ el.style.opacity = "";
1109
+ uncentre(el);
1110
+ };
1111
+ const hide = (el) => {
1112
+ touched.add(el);
1113
+ el.style.visibility = "hidden";
1114
+ el.style.opacity = "";
1115
+ uncentre(el);
1116
+ };
1117
+ let indexedRoot = null;
1118
+ let laidOut = true;
1119
+ let plans = /* @__PURE__ */ new Map();
1120
+ let placements = /* @__PURE__ */ new Map();
1121
+ /** Every stop below this has run; within the stop in hand, `finished` says. */
1122
+ let settledBelow = 0;
1123
+ const hasPlayed = (entry) => entry.stopIndex < settledBelow || finished.has(entry.item);
1124
+ /** Reads every element's standing from its effects, for a redraw or a seek. */
1125
+ const recompute = () => {
1126
+ for (const plan of plans.values()) {
1127
+ plan.top = null;
1128
+ for (const entry of plan.gates) if (hasPlayed(entry)) plan.top = entry;
1129
+ const first = plan.gates[0];
1130
+ plan.firstPlayed = first !== void 0 && hasPlayed(first);
1131
+ }
1132
+ };
1133
+ const index = () => {
1134
+ const current = root();
1135
+ const sample = plans.keys().next();
1136
+ if (current === indexedRoot && sample.done !== true && sample.value.isConnected && laidOut) return;
1137
+ indexedRoot = current;
1138
+ laidOut = true;
1139
+ silence(current);
1140
+ plans = /* @__PURE__ */ new Map();
1141
+ placements = /* @__PURE__ */ new Map();
1142
+ stops.forEach((stop, stopIndex) => {
1143
+ for (const item of stop.items) {
1144
+ const spots = [];
1145
+ for (const el of targetsOf(current, item.step, blocked)) {
1146
+ const plan = plans.get(el) ?? {
1147
+ list: [],
1148
+ gates: [],
1149
+ top: null,
1150
+ firstPlayed: false
1151
+ };
1152
+ plans.set(el, plan);
1153
+ const entry = {
1154
+ item,
1155
+ stopIndex,
1156
+ order: 0
1157
+ };
1158
+ plan.list.push(entry);
1159
+ if (item.kind !== "emphasis") plan.gates.push(entry);
1160
+ const movement = movementFor(item, el, current);
1161
+ if (movement === null && needsGeometry(item)) laidOut = false;
1162
+ spots.push({
1163
+ el,
1164
+ plan,
1165
+ entry,
1166
+ movement
1167
+ });
1168
+ }
1169
+ placements.set(item, spots);
1170
+ }
1171
+ });
1172
+ for (const plan of plans.values()) {
1173
+ const byBegin = (a, b) => a.stopIndex - b.stopIndex || a.item.begin - b.item.begin;
1174
+ plan.list.sort(byBegin);
1175
+ plan.gates.sort(byBegin);
1176
+ plan.list.forEach((entry, at) => {
1177
+ entry.order = at;
1178
+ });
1179
+ }
1180
+ recompute();
1181
+ };
1182
+ /** Where an element stands: the highest-priority effect that has run wins. */
1183
+ const visibilityOf = (plan) => {
1184
+ const first = plan.gates[0];
1185
+ if (first === void 0) return true;
1186
+ if (first.item.kind === "entrance" && !plan.firstPlayed) return false;
1187
+ return plan.top === null || plan.top.item.kind !== "exit";
1188
+ };
1189
+ /** An element an effect is animating is left to that effect until it ends. */
1190
+ const render = (el, plan) => {
1191
+ if (inFlight.has(el)) return;
1192
+ if (visibilityOf(plan)) show(el);
1193
+ else hide(el);
1194
+ };
1195
+ /**
1196
+ * Puts every animated element where it stands once `played` stops have run,
1197
+ * with no motion. Idempotent, so back, jump and a cancelled advance all land
1198
+ * on the same picture as playing forward to that point.
1199
+ */
1200
+ const settle = (played) => {
1201
+ finished.clear();
1202
+ settledBelow = played;
1203
+ index();
1204
+ recompute();
1205
+ for (const [el, plan] of plans) render(el, plan);
1206
+ };
1207
+ /**
1208
+ * One effect has had its say on one element. Only that element can have
1209
+ * changed, and only against the effect deciding it so far — so a stop of many
1210
+ * effects costs one comparison each, not a re-reading of the slide each.
1211
+ */
1212
+ const markPlayed = (spot) => {
1213
+ const { plan, entry } = spot;
1214
+ if (entry.item.kind !== "emphasis") {
1215
+ if (entry === plan.gates[0]) plan.firstPlayed = true;
1216
+ if (plan.top === null || entry.order > plan.top.order) plan.top = entry;
1217
+ }
1218
+ render(spot.el, plan);
1219
+ };
1220
+ /** One effect has had its say everywhere it reaches. */
1221
+ const complete = (item) => {
1222
+ finished.add(item);
1223
+ index();
1224
+ for (const spot of placements.get(item) ?? []) markPlayed(spot);
1225
+ };
1226
+ const play = (item, into) => {
1227
+ index();
1228
+ const spots = placements.get(item) ?? [];
1229
+ if (spots.length === 0) {
1230
+ complete(item);
1231
+ return;
1232
+ }
1233
+ const moving = spots.filter((spot) => spot.movement !== null);
1234
+ const duration = item.duration;
1235
+ const from = Math.max(0, into);
1236
+ const over = duration === null || duration === 0 || from >= duration;
1237
+ const holds = item.step.valueAfterEnd !== "removed";
1238
+ if (reduced() || moving.length === 0 || over && (!holds || !moving.some((spot) => inFlight.has(spot.el)))) {
1239
+ complete(item);
1240
+ return;
1241
+ }
1242
+ for (const spot of spots) if (spot.movement === null) markPlayed(spot);
1243
+ const length = over ? 0 : duration;
1244
+ outstanding.set(item, moving.length);
1245
+ for (const spot of moving) {
1246
+ const el = spot.el;
1247
+ const movement = spot.movement;
1248
+ touched.add(el);
1249
+ if (item.kind !== "emphasis") el.style.visibility = "";
1250
+ if (movement.aboutOwnCentre) centreOn(el);
1251
+ inFlight.set(el, (inFlight.get(el) ?? 0) + 1);
1252
+ const animation = el.animate(movement.frames, {
1253
+ duration: length,
1254
+ fill: "both"
1255
+ });
1256
+ animation.currentTime = over ? length : from;
1257
+ animations.push(animation);
1258
+ animation.finished.then(() => {
1259
+ if (disposed) return;
1260
+ drop(animations, animation);
1261
+ const left = (inFlight.get(el) ?? 1) - 1;
1262
+ if (left <= 0 || !holds) {
1263
+ if (left <= 0) {
1264
+ inFlight.delete(el);
1265
+ for (const other of frozen.get(el) ?? []) other.cancel();
1266
+ frozen.delete(el);
1267
+ } else inFlight.set(el, left);
1268
+ animation.cancel();
1269
+ } else {
1270
+ inFlight.set(el, left);
1271
+ const held = frozen.get(el);
1272
+ if (held === void 0) frozen.set(el, [animation]);
1273
+ else held.push(animation);
1274
+ }
1275
+ markPlayed(spot);
1276
+ const remaining = (outstanding.get(item) ?? 1) - 1;
1277
+ outstanding.set(item, remaining);
1278
+ if (remaining <= 0) complete(item);
1279
+ notifyChanged();
1280
+ }, () => {});
1281
+ }
1282
+ };
1283
+ const runStop = (stopIndex, elapsed) => {
1284
+ const stop = stops[stopIndex];
1285
+ if (stop === void 0) return;
1286
+ const since = Math.max(0, elapsed);
1287
+ startedAt = nowMs() - since;
1288
+ const ordered = [...stop.items].sort((a, b) => a.begin - b.begin);
1289
+ for (const item of ordered) {
1290
+ const wait = item.begin - since;
1291
+ if (reduced() || wait <= 0) {
1292
+ play(item, since - item.begin);
1293
+ continue;
1294
+ }
1295
+ const timer = setTimeout(() => {
1296
+ drop(timers, timer);
1297
+ play(item, 0);
1298
+ notifyChanged();
1299
+ }, wait);
1300
+ timers.push(timer);
1301
+ }
1302
+ };
1303
+ const clamp = (next) => Math.max(0, Math.min(Number.isFinite(next) ? Math.round(next) : 0, stops.length));
1304
+ const player = {
1305
+ get cursor() {
1306
+ return cursor;
1307
+ },
1308
+ get stopCount() {
1309
+ return stops.length;
1310
+ },
1311
+ get pending() {
1312
+ return cursor < stops.length;
1313
+ },
1314
+ get running() {
1315
+ return timers.length > 0 || animations.length > 0;
1316
+ },
1317
+ get progress() {
1318
+ return {
1319
+ cursor,
1320
+ stops: stops.length,
1321
+ elapsed: startedAt === null ? null : Math.round(nowMs() - startedAt),
1322
+ generation
1323
+ };
1324
+ },
1325
+ get unsupported() {
1326
+ return [...unsupported];
1327
+ },
1328
+ reset() {
1329
+ generation += 1;
1330
+ clearPending();
1331
+ startedAt = null;
1332
+ cursor = 0;
1333
+ settle(0);
1334
+ onChange(player);
1335
+ if (stops[0]?.auto === true) player.advance();
1336
+ },
1337
+ advance() {
1338
+ if (disposed || cursor >= stops.length) return false;
1339
+ generation += 1;
1340
+ clearPending();
1341
+ settle(cursor);
1342
+ const stopIndex = cursor;
1343
+ cursor += 1;
1344
+ runStop(stopIndex, 0);
1345
+ onChange(player);
1346
+ return true;
1347
+ },
1348
+ back() {
1349
+ if (disposed || cursor <= 0) return false;
1350
+ generation += 1;
1351
+ clearPending();
1352
+ startedAt = null;
1353
+ cursor -= 1;
1354
+ settle(cursor);
1355
+ onChange(player);
1356
+ return true;
1357
+ },
1358
+ jumpTo(next) {
1359
+ if (disposed) return;
1360
+ generation += 1;
1361
+ clearPending();
1362
+ startedAt = null;
1363
+ cursor = clamp(next);
1364
+ settle(cursor);
1365
+ onChange(player);
1366
+ },
1367
+ resume(next, elapsed) {
1368
+ if (disposed) return;
1369
+ generation += 1;
1370
+ clearPending();
1371
+ startedAt = null;
1372
+ cursor = clamp(next);
1373
+ if (cursor > 0 && elapsed !== null && Number.isFinite(elapsed)) {
1374
+ settle(cursor - 1);
1375
+ runStop(cursor - 1, elapsed);
1376
+ } else settle(cursor);
1377
+ onChange(player);
1378
+ },
1379
+ finish() {
1380
+ player.jumpTo(stops.length);
1381
+ },
1382
+ dispose() {
1383
+ if (disposed) return;
1384
+ disposed = true;
1385
+ clearPending();
1386
+ for (const el of touched) {
1387
+ el.style.visibility = "";
1388
+ el.style.opacity = "";
1389
+ uncentre(el);
1390
+ }
1391
+ touched.clear();
1392
+ }
1393
+ };
1394
+ index();
1395
+ return player;
1396
+ };
1397
+ //#endregion
1398
+ export { createAnimationPlayer as n, buildAnimationStops as t };
1399
+
1400
+ //# sourceMappingURL=animation-player-D2mt2MI8.js.map