domotion-svg 0.8.0 → 0.10.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.
- package/assets/fonts/LICENSE-last-resort-font.txt +94 -0
- package/assets/fonts/LastResortHE-Regular.ttf +0 -0
- package/dist/animation/animator.js +199 -216
- package/dist/animation/cursor-overlay.js +5 -12
- package/dist/animation/frame-timeline.d.ts +27 -0
- package/dist/animation/frame-timeline.js +26 -0
- package/dist/animation/magic-move.js +4 -4
- package/dist/capture/embed.js +9 -4
- package/dist/capture/emoji.js +7 -0
- package/dist/capture/index.js +61 -56
- package/dist/capture/initial-letter-probe.d.ts +37 -0
- package/dist/capture/initial-letter-probe.js +144 -0
- package/dist/capture/script/emoji-detect.d.ts +2 -2
- package/dist/capture/script/emoji-detect.js +72 -4
- package/dist/capture/script/index.js +28 -546
- package/dist/capture/script/utils.d.ts +7 -0
- package/dist/capture/script/utils.js +19 -0
- package/dist/capture/script/walker/counter-prewalk.d.ts +3 -0
- package/dist/capture/script/walker/counter-prewalk.js +155 -0
- package/dist/capture/script/walker/inline-svg.d.ts +1 -0
- package/dist/capture/script/walker/inline-svg.js +414 -0
- package/dist/capture/script/walker/input-value.d.ts +8 -0
- package/dist/capture/script/walker/input-value.js +153 -7
- package/dist/capture/script/walker/pseudo-content.d.ts +10 -7
- package/dist/capture/script/walker/pseudo-content.js +272 -247
- package/dist/capture/script/walker/pseudo-inject.js +6 -1
- package/dist/capture/script/walker/replaced-elements.js +3 -5
- package/dist/capture/script/walker/text-segments.d.ts +89 -26
- package/dist/capture/script/walker/text-segments.js +655 -217
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +93 -0
- package/dist/cli/animate-config-json-schema.d.ts +32 -0
- package/dist/cli/animate-config-json-schema.js +65 -0
- package/dist/cli/animate.d.ts +57 -8
- package/dist/cli/animate.js +43 -12
- package/dist/cli/common.d.ts +19 -0
- package/dist/cli/common.js +49 -0
- package/dist/cli/review.js +10 -30
- package/dist/cli/scrubber.d.ts +17 -0
- package/dist/cli/scrubber.js +91 -0
- package/dist/cli/svg-to-video-core.d.ts +3 -0
- package/dist/cli/svg-to-video-core.js +18 -6
- package/dist/cli/svg-to-video.js +11 -20
- package/dist/render/element-tree-to-svg.js +352 -174
- package/dist/render/glyph-helper.d.ts +73 -3
- package/dist/render/glyph-helper.js +409 -19
- package/dist/render/render-profile.d.ts +11 -0
- package/dist/render/render-profile.js +39 -0
- package/dist/render/text-to-path.d.ts +77 -2
- package/dist/render/text-to-path.js +1257 -358
- package/dist/render/text.d.ts +14 -0
- package/dist/render/text.js +88 -4
- package/dist/render/unicode-font-routing.darwin.generated.d.ts +8 -0
- package/dist/render/unicode-font-routing.darwin.generated.js +484 -0
- package/dist/render/unicode-font-routing.linux.generated.d.ts +8 -0
- package/dist/render/unicode-font-routing.linux.generated.js +350 -0
- package/dist/render/unicode-font-routing.win32.generated.d.ts +7 -0
- package/dist/render/unicode-font-routing.win32.generated.js +374 -0
- package/dist/render/vertical-text.d.ts +50 -0
- package/dist/render/vertical-text.js +284 -0
- package/dist/review/compare-pngs.js +42 -24
- package/dist/review/server.js +3 -1
- package/dist/scroll/composer.js +278 -208
- package/dist/scroll/executor.d.ts +26 -1
- package/dist/scroll/executor.js +70 -24
- package/dist/scroll/hoist-fixed.js +2 -25
- package/dist/scroll/hoist-sticky.js +2 -24
- package/dist/scroll/pattern.d.ts +5 -1
- package/dist/scrubber/client.bundle.generated.d.ts +1 -0
- package/dist/scrubber/client.bundle.generated.js +3 -0
- package/dist/scrubber/client.d.ts +12 -0
- package/dist/scrubber/client.js +485 -0
- package/dist/scrubber/server.d.ts +35 -0
- package/dist/scrubber/server.js +299 -0
- package/dist/scrubber/trim.d.ts +42 -0
- package/dist/scrubber/trim.js +351 -0
- package/dist/tree-ops/prune-tree.d.ts +14 -0
- package/dist/tree-ops/prune-tree.js +40 -0
- package/dist/tree-ops/tree-diff.d.ts +3 -0
- package/dist/tree-ops/tree-diff.js +7 -6
- package/dist/tree-ops/viewbox-culling.js +3 -2
- package/dist/utils/keyframe-pad.d.ts +24 -0
- package/dist/utils/keyframe-pad.js +28 -0
- package/dist/utils/wait-events.d.ts +50 -0
- package/dist/utils/wait-events.js +87 -0
- package/package.json +12 -4
- package/schemas/animate-config.schema.json +1253 -0
- package/dist/tree-ops/frame-merge.d.ts +0 -105
- package/dist/tree-ops/frame-merge.js +0 -395
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
This Font Software is licensed under the SIL Open Font License,
|
|
2
|
+
Version 1.1.
|
|
3
|
+
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font
|
|
14
|
+
creation efforts of academic and linguistic communities, and to
|
|
15
|
+
provide a free and open framework in which fonts may be shared and
|
|
16
|
+
improved in partnership with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply to
|
|
25
|
+
any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software
|
|
36
|
+
components as distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to,
|
|
39
|
+
deleting, or substituting -- in part or in whole -- any of the
|
|
40
|
+
components of the Original Version, by changing formats or by porting
|
|
41
|
+
the Font Software to a new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION & CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
49
|
+
modify, redistribute, and sell modified and unmodified copies of the
|
|
50
|
+
Font Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components, in
|
|
53
|
+
Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the
|
|
64
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
65
|
+
primary font name as presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created using
|
|
77
|
+
the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
93
|
+
|
|
94
|
+
SPDX-License-Identifier: OFL-1.1
|
|
Binary file
|
|
@@ -5,9 +5,156 @@
|
|
|
5
5
|
* animated SVG with CSS keyframe transitions.
|
|
6
6
|
*/
|
|
7
7
|
import { cursorOverlayMarkup, resolveCursorScript } from "./cursor-overlay.js";
|
|
8
|
+
import { escapeHtml } from "../utils/escapeHtml.js";
|
|
9
|
+
import { DEFAULT_TRANSITION_MS, frameAdvanceMs, transitionDurationMs } from "./frame-timeline.js";
|
|
10
|
+
import { KEYFRAME_EPSILON, padAfter, padBefore } from "../utils/keyframe-pad.js";
|
|
11
|
+
/**
|
|
12
|
+
* Emit one magic-move frame (DM-898): the frame-i blob (held [start..holdEnd],
|
|
13
|
+
* hard-cut out), the bridge composite (visible only across [holdEnd..transEnd]),
|
|
14
|
+
* the per-element slide / fade keyframes within that window, and the
|
|
15
|
+
* `prefers-reduced-motion` pinning (DM-901 / DM-903). Returns the SVG group
|
|
16
|
+
* fragments and the `@keyframes`/rule CSS for the caller to splice in.
|
|
17
|
+
* Extracted from `generateAnimatedSvg`'s per-frame loop (DM-1089) — byte-identical.
|
|
18
|
+
*/
|
|
19
|
+
function emitMagicMoveFrame(i, frame, mm, startPct, holdEndPct, transEndPct, totalSec) {
|
|
20
|
+
const groups = [];
|
|
21
|
+
const keyframes = [];
|
|
22
|
+
const sNum = parseFloat(startPct);
|
|
23
|
+
const hNum = parseFloat(holdEndPct);
|
|
24
|
+
const tNum = parseFloat(transEndPct);
|
|
25
|
+
const beforeS = padBefore(sNum, KEYFRAME_EPSILON.cull, 3);
|
|
26
|
+
const afterH = padAfter(hNum, KEYFRAME_EPSILON.cull, 3);
|
|
27
|
+
const beforeH = padBefore(hNum, KEYFRAME_EPSILON.cull, 3);
|
|
28
|
+
const afterT = padAfter(tNum, KEYFRAME_EPSILON.cull, 3);
|
|
29
|
+
// Frame i blob: visible only during its hold, hard-cut out at hold end.
|
|
30
|
+
groups.push(` <g class="f f-${i}">\n${frame.svgContent}\n </g>`);
|
|
31
|
+
keyframes.push(`
|
|
32
|
+
@keyframes fv-${i} {
|
|
33
|
+
0% { opacity: 0; visibility: hidden; }
|
|
34
|
+
${beforeS}% { opacity: 0; visibility: hidden; }
|
|
35
|
+
${sNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
36
|
+
${hNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
37
|
+
${afterH}% { opacity: 0; visibility: hidden; }
|
|
38
|
+
100% { opacity: 0; visibility: hidden; }
|
|
39
|
+
}
|
|
40
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite; animation-timing-function: step-end; }`);
|
|
41
|
+
// Bridge composite: visible during the transition window only.
|
|
42
|
+
groups.push(` <g class="f mm-${i}">\n${mm.compositeSvg}\n </g>`);
|
|
43
|
+
keyframes.push(`
|
|
44
|
+
@keyframes mmv-${i} {
|
|
45
|
+
0% { opacity: 0; visibility: hidden; }
|
|
46
|
+
${beforeH}% { opacity: 0; visibility: hidden; }
|
|
47
|
+
${hNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
48
|
+
${tNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
49
|
+
${afterT}% { opacity: 0; visibility: hidden; }
|
|
50
|
+
100% { opacity: 0; visibility: hidden; }
|
|
51
|
+
}
|
|
52
|
+
.mm-${i} { animation: mmv-${i} ${totalSec.toFixed(2)}s infinite; animation-timing-function: step-end; }`);
|
|
53
|
+
// Per-element slide / fade keyframes within the window (linear interp).
|
|
54
|
+
// The composite is only visible [holdEnd..transEnd], so the held values
|
|
55
|
+
// outside that window are never painted — they just pin the endpoints.
|
|
56
|
+
//
|
|
57
|
+
// A dual-render cross-fade copy (DM-903) is BOTH a slide and a fade, so
|
|
58
|
+
// its element needs two animations. They MUST go in one `animation:`
|
|
59
|
+
// declaration (comma-joined) — two separate `.cls { animation: … }` rules
|
|
60
|
+
// would have the later one silently override the former, dropping the
|
|
61
|
+
// slide. Accumulate per-class animation entries and emit one rule each.
|
|
62
|
+
const animEntries = new Map();
|
|
63
|
+
const addAnim = (cls, name) => {
|
|
64
|
+
const list = animEntries.get(cls) ?? [];
|
|
65
|
+
list.push(`${name} ${totalSec.toFixed(2)}s infinite`);
|
|
66
|
+
animEntries.set(cls, list);
|
|
67
|
+
};
|
|
68
|
+
for (const s of mm.slides) {
|
|
69
|
+
keyframes.push(`
|
|
70
|
+
@keyframes mms-${s.cls} {
|
|
71
|
+
0%, ${hNum.toFixed(3)}% { transform: ${s.from}; }
|
|
72
|
+
${tNum.toFixed(3)}%, 100% { transform: ${s.to}; }
|
|
73
|
+
}`);
|
|
74
|
+
addAnim(s.cls, `mms-${s.cls}`);
|
|
75
|
+
}
|
|
76
|
+
for (const cls of mm.fadeIn) {
|
|
77
|
+
keyframes.push(`
|
|
78
|
+
@keyframes mmf-${cls} {
|
|
79
|
+
0%, ${hNum.toFixed(3)}% { opacity: 0; }
|
|
80
|
+
${tNum.toFixed(3)}%, 100% { opacity: 1; }
|
|
81
|
+
}`);
|
|
82
|
+
addAnim(cls, `mmf-${cls}`);
|
|
83
|
+
}
|
|
84
|
+
for (const cls of mm.fadeOut) {
|
|
85
|
+
keyframes.push(`
|
|
86
|
+
@keyframes mmf-${cls} {
|
|
87
|
+
0%, ${hNum.toFixed(3)}% { opacity: 1; }
|
|
88
|
+
${tNum.toFixed(3)}%, 100% { opacity: 0; }
|
|
89
|
+
}`);
|
|
90
|
+
addAnim(cls, `mmf-${cls}`);
|
|
91
|
+
}
|
|
92
|
+
for (const [cls, entries] of animEntries) {
|
|
93
|
+
keyframes.push(` .${cls} { animation: ${entries.join(", ")}; }`);
|
|
94
|
+
}
|
|
95
|
+
// DM-901: honor `prefers-reduced-motion: reduce` — pin everything to the
|
|
96
|
+
// NEXT state instead of animating, so the transition degrades to a
|
|
97
|
+
// cut-like reveal for motion-sensitive viewers.
|
|
98
|
+
const reduceRules = [];
|
|
99
|
+
if (mm.slides.length > 0)
|
|
100
|
+
reduceRules.push(`${mm.slides.map((s) => `.${s.cls}`).join(", ")} { animation: none; transform: none; }`);
|
|
101
|
+
if (mm.fadeIn.length > 0)
|
|
102
|
+
reduceRules.push(`${mm.fadeIn.map((c) => `.${c}`).join(", ")} { animation: none; opacity: 1; }`);
|
|
103
|
+
if (mm.fadeOut.length > 0)
|
|
104
|
+
reduceRules.push(`${mm.fadeOut.map((c) => `.${c}`).join(", ")} { animation: none; opacity: 0; }`);
|
|
105
|
+
if (reduceRules.length > 0) {
|
|
106
|
+
keyframes.push(`
|
|
107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
108
|
+
${reduceRules.join("\n ")}
|
|
109
|
+
}`);
|
|
110
|
+
}
|
|
111
|
+
return { groups, keyframes };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Emit one crossfade or cut frame (the default transition path): the frame
|
|
115
|
+
* blob plus its opacity keyframes. `cut` (or zero-duration) uses disjoint
|
|
116
|
+
* step-end keyframes so opacity flips instantly with no interpolation smear;
|
|
117
|
+
* crossfade overlaps the fade-in with the previous frame's fade-out, with the
|
|
118
|
+
* visible window driven by `fadeInStartPct` (precomputed by the caller, which
|
|
119
|
+
* knows the overlap state). Extracted from `generateAnimatedSvg` (DM-1089).
|
|
120
|
+
*/
|
|
121
|
+
function emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEndPct, transEndPct, fadeInStartPct, totalSec) {
|
|
122
|
+
const groups = [];
|
|
123
|
+
const keyframes = [];
|
|
124
|
+
groups.push(` <g class="f f-${i}">\n${frame.svgContent}\n </g>`);
|
|
125
|
+
const isCut = transType === "cut" || transDur === 0;
|
|
126
|
+
if (isCut) {
|
|
127
|
+
const startNum = parseFloat(startPct);
|
|
128
|
+
const endNum = parseFloat(transEndPct);
|
|
129
|
+
const beforeStart = padBefore(startNum, KEYFRAME_EPSILON.cull, 3);
|
|
130
|
+
const afterEnd = padAfter(endNum, KEYFRAME_EPSILON.cull, 3);
|
|
131
|
+
keyframes.push(`
|
|
132
|
+
@keyframes fv-${i} {
|
|
133
|
+
0% { opacity: 0; visibility: hidden; }
|
|
134
|
+
${beforeStart}% { opacity: 0; visibility: hidden; }
|
|
135
|
+
${startNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
136
|
+
${endNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
137
|
+
${afterEnd}% { opacity: 0; visibility: hidden; }
|
|
138
|
+
100% { opacity: 0; visibility: hidden; }
|
|
139
|
+
}
|
|
140
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite; animation-timing-function: step-end; }`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const prevEnd = i > 0
|
|
144
|
+
? `${padBefore(parseFloat(fadeInStartPct), KEYFRAME_EPSILON.display, 2)}%,`
|
|
145
|
+
: "";
|
|
146
|
+
keyframes.push(`
|
|
147
|
+
@keyframes fv-${i} {
|
|
148
|
+
0%, ${prevEnd} ${transEndPct}, 100% { opacity: 0; }
|
|
149
|
+
${startPct}, ${holdEndPct} { opacity: 1; }
|
|
150
|
+
}${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, transEndPct)}
|
|
151
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }`);
|
|
152
|
+
}
|
|
153
|
+
return { groups, keyframes };
|
|
154
|
+
}
|
|
8
155
|
export function generateAnimatedSvg(config) {
|
|
9
156
|
const { width, height, frames } = config;
|
|
10
|
-
const totalDuration = frames.reduce((sum, f) => sum +
|
|
157
|
+
const totalDuration = frames.reduce((sum, f) => sum + frameAdvanceMs(f), 0);
|
|
11
158
|
const totalSec = totalDuration / 1000;
|
|
12
159
|
// Pre-compute per-frame timing windows (used by both the merge pipeline for
|
|
13
160
|
// timeline keyframes and the atomic push/scroll fallbacks below).
|
|
@@ -17,7 +164,7 @@ export function generateAnimatedSvg(config) {
|
|
|
17
164
|
{
|
|
18
165
|
let t = 0;
|
|
19
166
|
for (const f of frames) {
|
|
20
|
-
const td =
|
|
167
|
+
const td = transitionDurationMs(f);
|
|
21
168
|
frameTiming.startPct.push((t / totalDuration) * 100);
|
|
22
169
|
frameTiming.holdEndPct.push(((t + f.duration) / totalDuration) * 100);
|
|
23
170
|
frameTiming.transEndPct.push(((t + f.duration + td) / totalDuration) * 100);
|
|
@@ -43,7 +190,7 @@ export function generateAnimatedSvg(config) {
|
|
|
43
190
|
let timeOffset = 0;
|
|
44
191
|
for (let i = 0; i < frames.length; i++) {
|
|
45
192
|
const frame = frames[i];
|
|
46
|
-
const transDur =
|
|
193
|
+
const transDur = transitionDurationMs(frame);
|
|
47
194
|
const transType = frame.transition?.type ?? "crossfade";
|
|
48
195
|
const startPct = pct(timeOffset, totalDuration);
|
|
49
196
|
const holdEndPct = pct(timeOffset + frame.duration, totalDuration);
|
|
@@ -61,7 +208,7 @@ export function generateAnimatedSvg(config) {
|
|
|
61
208
|
// one slides out, so its show window starts at `timeOffset - prevTransDur`
|
|
62
209
|
// rather than at `startPct`.
|
|
63
210
|
const entersViaOverlap = entersViaPush || entersViaScroll;
|
|
64
|
-
const prevTransDur = prevFrame != null ?
|
|
211
|
+
const prevTransDur = prevFrame != null ? transitionDurationMs(prevFrame) : DEFAULT_TRANSITION_MS;
|
|
65
212
|
const enterStartPct = entersViaOverlap
|
|
66
213
|
? pct(timeOffset - prevTransDur, totalDuration)
|
|
67
214
|
: startPct;
|
|
@@ -74,24 +221,7 @@ export function generateAnimatedSvg(config) {
|
|
|
74
221
|
// Window is [enterStartPct .. transEndPct] (when the slide has fully
|
|
75
222
|
// exited the viewBox); 0.01% pad on each side keeps the snap inside the
|
|
76
223
|
// existing opacity:0 bookend.
|
|
77
|
-
|
|
78
|
-
const visEnd = transEndPct;
|
|
79
|
-
keyframes.push(`
|
|
80
|
-
@keyframes fp-${i} {
|
|
81
|
-
0%, ${Math.max(0, parseFloat(enterStartPct) - 0.1).toFixed(2)}% { transform: translateX(${entersViaPush ? width : 0}px); }
|
|
82
|
-
${startPct} { transform: translateX(0); }
|
|
83
|
-
${holdEndPct} { transform: translateX(0); }
|
|
84
|
-
${transEndPct} { transform: translateX(-${width}px); }
|
|
85
|
-
${Math.min(100, parseFloat(transEndPct) + 0.1).toFixed(2)}%, 100% { transform: translateX(-${width}px); }
|
|
86
|
-
}
|
|
87
|
-
@keyframes fv-${i} {
|
|
88
|
-
0%, ${Math.max(0, parseFloat(enterStartPct) - 0.1).toFixed(2)}% { opacity: 0; }
|
|
89
|
-
${enterStartPct} { opacity: 1; }
|
|
90
|
-
${transEndPct} { opacity: 1; }
|
|
91
|
-
${Math.min(100, parseFloat(transEndPct) + 0.1).toFixed(2)}%, 100% { opacity: 0; }
|
|
92
|
-
}${buildDisplayKeyframes(`fd-${i}`, visStart, visEnd)}
|
|
93
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
94
|
-
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`);
|
|
224
|
+
keyframes.push(slideKeyframes(i, "X", width, entersViaPush, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec));
|
|
95
225
|
}
|
|
96
226
|
else if (transType === "scroll") {
|
|
97
227
|
// DM-609: `scroll` now means real geometric scroll between two frames
|
|
@@ -100,27 +230,10 @@ export function generateAnimatedSvg(config) {
|
|
|
100
230
|
// slides up from the bottom of the viewport, outgoing slides up off
|
|
101
231
|
// the top. Uses height instead of width and translateY instead of
|
|
102
232
|
// translateX, otherwise identical machinery (incl. the cull-friendly
|
|
103
|
-
// `fd-${i}` display animation).
|
|
104
|
-
|
|
233
|
+
// `fd-${i}` display animation). (`entersViaScroll` is already computed in
|
|
234
|
+
// the outer scope above — same value, no need to redeclare/shadow it.)
|
|
105
235
|
frameGroups.push(` <g class="f f-${i}"><clipPath id="fc-${i}"><rect width="${width}" height="${height}" /></clipPath><g clip-path="url(#fc-${i})" class="fp fp-${i}">\n${frame.svgContent}\n </g></g>`);
|
|
106
|
-
|
|
107
|
-
const visEnd = transEndPct;
|
|
108
|
-
keyframes.push(`
|
|
109
|
-
@keyframes fp-${i} {
|
|
110
|
-
0%, ${Math.max(0, parseFloat(enterStartPct) - 0.1).toFixed(2)}% { transform: translateY(${entersViaScroll ? height : 0}px); }
|
|
111
|
-
${startPct} { transform: translateY(0); }
|
|
112
|
-
${holdEndPct} { transform: translateY(0); }
|
|
113
|
-
${transEndPct} { transform: translateY(-${height}px); }
|
|
114
|
-
${Math.min(100, parseFloat(transEndPct) + 0.1).toFixed(2)}%, 100% { transform: translateY(-${height}px); }
|
|
115
|
-
}
|
|
116
|
-
@keyframes fv-${i} {
|
|
117
|
-
0%, ${Math.max(0, parseFloat(enterStartPct) - 0.1).toFixed(2)}% { opacity: 0; }
|
|
118
|
-
${enterStartPct} { opacity: 1; }
|
|
119
|
-
${transEndPct} { opacity: 1; }
|
|
120
|
-
${Math.min(100, parseFloat(transEndPct) + 0.1).toFixed(2)}%, 100% { opacity: 0; }
|
|
121
|
-
}${buildDisplayKeyframes(`fd-${i}`, visStart, visEnd)}
|
|
122
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
123
|
-
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`);
|
|
236
|
+
keyframes.push(slideKeyframes(i, "Y", height, entersViaScroll, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec));
|
|
124
237
|
}
|
|
125
238
|
else if (transType === "magic-move" && frame.magicMove != null) {
|
|
126
239
|
// DM-898: magic-move. Frame i holds [start..holdEnd] then HARD-CUTS out;
|
|
@@ -131,163 +244,21 @@ export function generateAnimatedSvg(config) {
|
|
|
131
244
|
// final paint and its end state the next frame's initial paint, so both
|
|
132
245
|
// hard cuts are seamless. (When `frame.magicMove` is null the type falls
|
|
133
246
|
// through to the crossfade branch below — the documented fallback.)
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const tNum = parseFloat(transEndPct);
|
|
138
|
-
const beforeS = Math.max(0, sNum - 0.001).toFixed(3);
|
|
139
|
-
const afterH = Math.min(100, hNum + 0.001).toFixed(3);
|
|
140
|
-
const beforeH = Math.max(0, hNum - 0.001).toFixed(3);
|
|
141
|
-
const afterT = Math.min(100, tNum + 0.001).toFixed(3);
|
|
142
|
-
// Frame i blob: visible only during its hold, hard-cut out at hold end.
|
|
143
|
-
frameGroups.push(` <g class="f f-${i}">\n${frame.svgContent}\n </g>`);
|
|
144
|
-
keyframes.push(`
|
|
145
|
-
@keyframes fv-${i} {
|
|
146
|
-
0% { opacity: 0; visibility: hidden; }
|
|
147
|
-
${beforeS}% { opacity: 0; visibility: hidden; }
|
|
148
|
-
${sNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
149
|
-
${hNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
150
|
-
${afterH}% { opacity: 0; visibility: hidden; }
|
|
151
|
-
100% { opacity: 0; visibility: hidden; }
|
|
152
|
-
}
|
|
153
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite; animation-timing-function: step-end; }`);
|
|
154
|
-
// Bridge composite: visible during the transition window only.
|
|
155
|
-
frameGroups.push(` <g class="f mm-${i}">\n${mm.compositeSvg}\n </g>`);
|
|
156
|
-
keyframes.push(`
|
|
157
|
-
@keyframes mmv-${i} {
|
|
158
|
-
0% { opacity: 0; visibility: hidden; }
|
|
159
|
-
${beforeH}% { opacity: 0; visibility: hidden; }
|
|
160
|
-
${hNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
161
|
-
${tNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
162
|
-
${afterT}% { opacity: 0; visibility: hidden; }
|
|
163
|
-
100% { opacity: 0; visibility: hidden; }
|
|
164
|
-
}
|
|
165
|
-
.mm-${i} { animation: mmv-${i} ${totalSec.toFixed(2)}s infinite; animation-timing-function: step-end; }`);
|
|
166
|
-
// Per-element slide / fade keyframes within the window (linear interp).
|
|
167
|
-
// The composite is only visible [holdEnd..transEnd], so the held values
|
|
168
|
-
// outside that window are never painted — they just pin the endpoints.
|
|
169
|
-
//
|
|
170
|
-
// A dual-render cross-fade copy (DM-903) is BOTH a slide and a fade, so
|
|
171
|
-
// its element needs two animations. They MUST go in one `animation:`
|
|
172
|
-
// declaration (comma-joined) — two separate `.cls { animation: … }` rules
|
|
173
|
-
// would have the later one silently override the former, dropping the
|
|
174
|
-
// slide. Accumulate per-class animation entries and emit one rule each.
|
|
175
|
-
const animEntries = new Map();
|
|
176
|
-
const addAnim = (cls, name) => {
|
|
177
|
-
const list = animEntries.get(cls) ?? [];
|
|
178
|
-
list.push(`${name} ${totalSec.toFixed(2)}s infinite`);
|
|
179
|
-
animEntries.set(cls, list);
|
|
180
|
-
};
|
|
181
|
-
for (const s of mm.slides) {
|
|
182
|
-
// Interpolate the element's transform `from → to` across the window.
|
|
183
|
-
// The next-appearance copy maps its prev rect → `none` (final next
|
|
184
|
-
// rect); a cross-fade prev copy maps `none` → its next rect, so both
|
|
185
|
-
// copies trace the same path (DM-899 geometry; DM-903 paired copies).
|
|
186
|
-
keyframes.push(`
|
|
187
|
-
@keyframes mms-${s.cls} {
|
|
188
|
-
0%, ${hNum.toFixed(3)}% { transform: ${s.from}; }
|
|
189
|
-
${tNum.toFixed(3)}%, 100% { transform: ${s.to}; }
|
|
190
|
-
}`);
|
|
191
|
-
addAnim(s.cls, `mms-${s.cls}`);
|
|
192
|
-
}
|
|
193
|
-
for (const cls of mm.fadeIn) {
|
|
194
|
-
keyframes.push(`
|
|
195
|
-
@keyframes mmf-${cls} {
|
|
196
|
-
0%, ${hNum.toFixed(3)}% { opacity: 0; }
|
|
197
|
-
${tNum.toFixed(3)}%, 100% { opacity: 1; }
|
|
198
|
-
}`);
|
|
199
|
-
addAnim(cls, `mmf-${cls}`);
|
|
200
|
-
}
|
|
201
|
-
for (const cls of mm.fadeOut) {
|
|
202
|
-
keyframes.push(`
|
|
203
|
-
@keyframes mmf-${cls} {
|
|
204
|
-
0%, ${hNum.toFixed(3)}% { opacity: 1; }
|
|
205
|
-
${tNum.toFixed(3)}%, 100% { opacity: 0; }
|
|
206
|
-
}`);
|
|
207
|
-
addAnim(cls, `mmf-${cls}`);
|
|
208
|
-
}
|
|
209
|
-
for (const [cls, entries] of animEntries) {
|
|
210
|
-
keyframes.push(` .${cls} { animation: ${entries.join(", ")}; }`);
|
|
211
|
-
}
|
|
212
|
-
// DM-901: honor `prefers-reduced-motion: reduce` — pin everything to the
|
|
213
|
-
// NEXT state instead of animating, so the transition degrades to a
|
|
214
|
-
// cut-like reveal for motion-sensitive viewers. Slides drop to their
|
|
215
|
-
// final transform (`none` for the next copy; the prev cross-fade copy is
|
|
216
|
-
// also hidden via its fade-out below). Added / next-appearance fades snap
|
|
217
|
-
// to opacity 1; removed / prev-appearance fades snap to opacity 0. Static
|
|
218
|
-
// CSS, so output stays deterministic; rasterizers default to
|
|
219
|
-
// `no-preference` and play the full move. (DM-903: the fade rules now
|
|
220
|
-
// also matter — without pinning fade-out to 0 the prev-appearance copy
|
|
221
|
-
// would stay visible at full opacity.)
|
|
222
|
-
const reduceRules = [];
|
|
223
|
-
if (mm.slides.length > 0)
|
|
224
|
-
reduceRules.push(`${mm.slides.map((s) => `.${s.cls}`).join(", ")} { animation: none; transform: none; }`);
|
|
225
|
-
if (mm.fadeIn.length > 0)
|
|
226
|
-
reduceRules.push(`${mm.fadeIn.map((c) => `.${c}`).join(", ")} { animation: none; opacity: 1; }`);
|
|
227
|
-
if (mm.fadeOut.length > 0)
|
|
228
|
-
reduceRules.push(`${mm.fadeOut.map((c) => `.${c}`).join(", ")} { animation: none; opacity: 0; }`);
|
|
229
|
-
if (reduceRules.length > 0) {
|
|
230
|
-
keyframes.push(`
|
|
231
|
-
@media (prefers-reduced-motion: reduce) {
|
|
232
|
-
${reduceRules.join("\n ")}
|
|
233
|
-
}`);
|
|
234
|
-
}
|
|
247
|
+
const r = emitMagicMoveFrame(i, frame, frame.magicMove, startPct, holdEndPct, transEndPct, totalSec);
|
|
248
|
+
frameGroups.push(...r.groups);
|
|
249
|
+
keyframes.push(...r.keyframes);
|
|
235
250
|
}
|
|
236
251
|
else {
|
|
237
|
-
// Crossfade or cut: opacity in/out.
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// is what we want here.
|
|
248
|
-
frameGroups.push(` <g class="f f-${i}">\n${frame.svgContent}\n </g>`);
|
|
249
|
-
const isCut = transType === "cut" || transDur === 0;
|
|
250
|
-
if (isCut) {
|
|
251
|
-
const startNum = parseFloat(startPct);
|
|
252
|
-
const endNum = parseFloat(transEndPct);
|
|
253
|
-
const beforeStart = Math.max(0, startNum - 0.001).toFixed(3);
|
|
254
|
-
const afterEnd = Math.min(100, endNum + 0.001).toFixed(3);
|
|
255
|
-
// DM-599: cut already uses step-end on the opacity animation, so we
|
|
256
|
-
// fold visibility into the same keyframes block — both snap together.
|
|
257
|
-
// DM-641: this used to toggle `display`. The base `.f { display: none }`
|
|
258
|
-
// rule kept the element out of the render tree at t=0, and Chromium
|
|
259
|
-
// doesn't tick infinite animations on out-of-tree elements — so the
|
|
260
|
-
// 0% keyframe never ran and the frame stayed permanently hidden.
|
|
261
|
-
// Switching to `visibility` leaves the element in the render tree
|
|
262
|
-
// (still skips painting, which was the DM-599 goal) so the animation
|
|
263
|
-
// ticks normally.
|
|
264
|
-
keyframes.push(`
|
|
265
|
-
@keyframes fv-${i} {
|
|
266
|
-
0% { opacity: 0; visibility: hidden; }
|
|
267
|
-
${beforeStart}% { opacity: 0; visibility: hidden; }
|
|
268
|
-
${startNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
269
|
-
${endNum.toFixed(3)}% { opacity: 1; visibility: visible; }
|
|
270
|
-
${afterEnd}% { opacity: 0; visibility: hidden; }
|
|
271
|
-
100% { opacity: 0; visibility: hidden; }
|
|
272
|
-
}
|
|
273
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite; animation-timing-function: step-end; }`);
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
const fadeInStartPct = (i > 0 && !entersViaMagicMove)
|
|
277
|
-
? pct(Math.max(0, timeOffset - prevTransDur), totalDuration)
|
|
278
|
-
: startPct;
|
|
279
|
-
const prevEnd = i > 0
|
|
280
|
-
? `${Math.max(0, parseFloat(fadeInStartPct) - 0.01).toFixed(2)}%,`
|
|
281
|
-
: "";
|
|
282
|
-
// DM-599: visible window spans the full fade — fadeInStart through
|
|
283
|
-
// transEnd (display stays `inline` while opacity interpolates).
|
|
284
|
-
keyframes.push(`
|
|
285
|
-
@keyframes fv-${i} {
|
|
286
|
-
0%, ${prevEnd} ${transEndPct}, 100% { opacity: 0; }
|
|
287
|
-
${startPct}, ${holdEndPct} { opacity: 1; }
|
|
288
|
-
}${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, transEndPct)}
|
|
289
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }`);
|
|
290
|
-
}
|
|
252
|
+
// Crossfade or cut: opacity in/out (see emitCrossfadeOrCutFrame). The
|
|
253
|
+
// crossfade fade-in OVERLAPS the previous frame's fade-out, so its visible
|
|
254
|
+
// window starts at fadeInStartPct — which depends on the loop's overlap
|
|
255
|
+
// state (entersViaMagicMove / prevTransDur), so it's computed here.
|
|
256
|
+
const fadeInStartPct = (i > 0 && !entersViaMagicMove)
|
|
257
|
+
? pct(Math.max(0, timeOffset - prevTransDur), totalDuration)
|
|
258
|
+
: startPct;
|
|
259
|
+
const r = emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEndPct, transEndPct, fadeInStartPct, totalSec);
|
|
260
|
+
frameGroups.push(...r.groups);
|
|
261
|
+
keyframes.push(...r.keyframes);
|
|
291
262
|
}
|
|
292
263
|
// Overlays
|
|
293
264
|
if (frame.overlays != null) {
|
|
@@ -332,7 +303,7 @@ export function generateAnimatedSvg(config) {
|
|
|
332
303
|
let acc = 0;
|
|
333
304
|
for (const f of frames) {
|
|
334
305
|
frameStarts.push(acc);
|
|
335
|
-
acc +=
|
|
306
|
+
acc += frameAdvanceMs(f);
|
|
336
307
|
}
|
|
337
308
|
const resolved = resolveCursorScript(config.cursorOverlay, totalDuration, frameStarts, config.resolveSelector ?? null);
|
|
338
309
|
overlayMarkup = "\n" + cursorOverlayMarkup(resolved.positions, resolved.clicks, resolved.style, totalDuration);
|
|
@@ -360,18 +331,6 @@ ${canvasBgRect}${frameGroups.join("\n")}${overlayMarkup}
|
|
|
360
331
|
</svg>`;
|
|
361
332
|
return out;
|
|
362
333
|
}
|
|
363
|
-
/**
|
|
364
|
-
* Effective transition duration for a frame. `cut` is always 0 — the type
|
|
365
|
-
* means "instant" so any duration on the input is meaningless. Default
|
|
366
|
-
* (no transition specified) is 300ms (legacy crossfade duration).
|
|
367
|
-
*/
|
|
368
|
-
function transitionDuration(f) {
|
|
369
|
-
if (f.transition == null)
|
|
370
|
-
return 300;
|
|
371
|
-
if (f.transition.type === "cut")
|
|
372
|
-
return 0;
|
|
373
|
-
return f.transition.duration;
|
|
374
|
-
}
|
|
375
334
|
/**
|
|
376
335
|
* Wrap `text` into lines no wider than `maxChars` monospace cells, the way a
|
|
377
336
|
* browser textarea does: break on spaces, char-break a word longer than the
|
|
@@ -481,7 +440,7 @@ function renderTypingOverlay(overlay, frameIdx, frameStart, frameEnd, totalDurat
|
|
|
481
440
|
lineTimings.push({ li, startMs: lineStartMs, endMs: lineEndMs, len: line.length });
|
|
482
441
|
cumChars += line.length;
|
|
483
442
|
parts.push(` <defs><clipPath id="${clipId}"><rect class="${id}-rev${li}" x="${overlay.x}" y="${lineY - fontSize}" width="0" height="${textHeight}" /></clipPath></defs>`);
|
|
484
|
-
parts.push(` <text class="${id}-text" x="${overlay.x}" y="${lineY}" fill="${color}" font-size="${fontSize}" font-family="'SF Mono', Menlo, Monaco, monospace" clip-path="url(#${clipId})">${
|
|
443
|
+
parts.push(` <text class="${id}-text" x="${overlay.x}" y="${lineY}" fill="${color}" font-size="${fontSize}" font-family="'SF Mono', Menlo, Monaco, monospace" clip-path="url(#${clipId})">${escapeHtml(line)}</text>`);
|
|
485
444
|
cssRules.push(`
|
|
486
445
|
@keyframes ${id}-rev${li} { 0%, ${lineStartPct} { width: 0; } ${lineEndPct} { width: ${lineWidth}px; } ${holdEndPct} { width: ${lineWidth}px; } ${disappearPct}, 100% { width: 0; } }
|
|
487
446
|
.${id}-rev${li} { animation: ${id}-rev${li} ${totalSec.toFixed(2)}s infinite; }`);
|
|
@@ -598,8 +557,8 @@ function buildDisplayKeyframes(name, visibleStartPct, visibleEndPct) {
|
|
|
598
557
|
// ticks in Chromium.
|
|
599
558
|
const start = parseFloat(String(visibleStartPct));
|
|
600
559
|
const end = parseFloat(String(visibleEndPct));
|
|
601
|
-
const startMinus =
|
|
602
|
-
const endPlus =
|
|
560
|
+
const startMinus = padBefore(start, KEYFRAME_EPSILON.display, 3);
|
|
561
|
+
const endPlus = padAfter(end, KEYFRAME_EPSILON.display, 3);
|
|
603
562
|
return `
|
|
604
563
|
@keyframes ${name} {
|
|
605
564
|
0% { visibility: hidden; }
|
|
@@ -610,6 +569,33 @@ function buildDisplayKeyframes(name, visibleStartPct, visibleEndPct) {
|
|
|
610
569
|
100% { visibility: hidden; }
|
|
611
570
|
}`;
|
|
612
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Slide-transition keyframes (push-left / scroll). The two transitions are the
|
|
574
|
+
* same machinery on different axes: `push-left` slides horizontally (axis `X`,
|
|
575
|
+
* `size` = width), `scroll` slides vertically (axis `Y`, `size` = height). The
|
|
576
|
+
* incoming frame starts off-screen (`+size`) only when the predecessor was the
|
|
577
|
+
* same slide type (`entersSliding`), holds at 0 across its show window, then
|
|
578
|
+
* exits to `-size`. 0.1% pads on each bookend keep the snap inside the
|
|
579
|
+
* opacity:0 frame. Emits the fp/fv/fd keyframes + the `.f-`/`.fp-` rules.
|
|
580
|
+
*/
|
|
581
|
+
function slideKeyframes(i, axis, size, entersSliding, enterStartPct, startPct, holdEndPct, transEndPct, visStart, visEnd, totalSec) {
|
|
582
|
+
return `
|
|
583
|
+
@keyframes fp-${i} {
|
|
584
|
+
0%, ${padBefore(parseFloat(enterStartPct), KEYFRAME_EPSILON.slide, 2)}% { transform: translate${axis}(${entersSliding ? size : 0}px); }
|
|
585
|
+
${startPct} { transform: translate${axis}(0); }
|
|
586
|
+
${holdEndPct} { transform: translate${axis}(0); }
|
|
587
|
+
${transEndPct} { transform: translate${axis}(-${size}px); }
|
|
588
|
+
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { transform: translate${axis}(-${size}px); }
|
|
589
|
+
}
|
|
590
|
+
@keyframes fv-${i} {
|
|
591
|
+
0%, ${padBefore(parseFloat(enterStartPct), KEYFRAME_EPSILON.slide, 2)}% { opacity: 0; }
|
|
592
|
+
${enterStartPct} { opacity: 1; }
|
|
593
|
+
${transEndPct} { opacity: 1; }
|
|
594
|
+
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { opacity: 0; }
|
|
595
|
+
}${buildDisplayKeyframes(`fd-${i}`, visStart, visEnd)}
|
|
596
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
597
|
+
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`;
|
|
598
|
+
}
|
|
613
599
|
/**
|
|
614
600
|
* Render a frame-local SVG overlay. The embedded SVG markup is wrapped in a
|
|
615
601
|
* `<g transform="translate(x y)" clip-path="..."/>` and an inner
|
|
@@ -744,6 +730,3 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
744
730
|
}
|
|
745
731
|
return out.length === 0 ? "" : "\n" + out.join("\n");
|
|
746
732
|
}
|
|
747
|
-
function escapeXml(s) {
|
|
748
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
749
|
-
}
|
|
@@ -68,19 +68,12 @@ export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, r
|
|
|
68
68
|
const target = resolveMoveTarget(ev, curX, curY, frameForT(ev.t), resolveSelector);
|
|
69
69
|
if (target == null)
|
|
70
70
|
continue;
|
|
71
|
-
// First time we ever position the cursor — also turn it visible.
|
|
72
|
-
const becomeVisible = !visible;
|
|
73
71
|
if (dur > 0) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
pushKey(ev.t, curX, curY, true);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
pushKey(ev.t, curX, curY, true);
|
|
83
|
-
}
|
|
72
|
+
// Anchor the cursor at its previous spot when the move begins, then
|
|
73
|
+
// interpolate to the target over `dur`, so it slides rather than popping
|
|
74
|
+
// in mid-frame. (Both the first-positioning and subsequent-move cases
|
|
75
|
+
// emit the same start keyframe — DM-1073 collapsed a no-op branch here.)
|
|
76
|
+
pushKey(ev.t, curX, curY, true);
|
|
84
77
|
pushKey(ev.t + dur, target.x, target.y, true);
|
|
85
78
|
}
|
|
86
79
|
else {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared frame-timeline math for the animator and the `animate` CLI. The
|
|
3
|
+
* "cut → 0, else the transition's duration, else the default" rule and the
|
|
4
|
+
* default transition length lived in BOTH `animator.ts` (as `transitionDuration`
|
|
5
|
+
* + `DEFAULT_TRANSITION_MS`) and `cli/animate.ts` (open-coded with a literal
|
|
6
|
+
* `300` in three places) — exactly the kind of copy that drifts. Both now share
|
|
7
|
+
* these so the scene clock advances identically wherever it's computed.
|
|
8
|
+
*/
|
|
9
|
+
/** Default transition length (ms) when a frame specifies no transition — the
|
|
10
|
+
* legacy crossfade duration. */
|
|
11
|
+
export declare const DEFAULT_TRANSITION_MS = 300;
|
|
12
|
+
/** The minimal frame shape this math needs — satisfied by both the animator's
|
|
13
|
+
* `AnimationFrame` and the CLI's parsed frame config. */
|
|
14
|
+
export interface TimelineFrame {
|
|
15
|
+
duration: number;
|
|
16
|
+
transition?: {
|
|
17
|
+
type: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
} | null;
|
|
20
|
+
}
|
|
21
|
+
/** Effective transition duration for a frame. `cut` is always 0 (the type means
|
|
22
|
+
* "instant", so any input duration is meaningless); no transition → the
|
|
23
|
+
* default. */
|
|
24
|
+
export declare function transitionDurationMs(f: TimelineFrame): number;
|
|
25
|
+
/** How far the scene clock advances across one frame: its hold duration plus its
|
|
26
|
+
* outgoing transition. The unit of every frame-timeline accumulation. */
|
|
27
|
+
export declare function frameAdvanceMs(f: TimelineFrame): number;
|