animot-presenter 0.5.5 → 0.5.6
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/dist/AnimotPresenter.svelte +5 -1
- package/dist/cdn/animot-presenter.esm.js +4530 -4334
- package/dist/cdn/animot-presenter.min.js +8 -8
- package/dist/engine/utils.d.ts +11 -2
- package/dist/engine/utils.js +16 -4
- package/dist/types.d.ts +12 -2
- package/dist/utils/text-animate.d.ts +47 -0
- package/dist/utils/text-animate.js +348 -0
- package/package.json +1 -1
package/dist/engine/utils.d.ts
CHANGED
|
@@ -15,13 +15,22 @@ export declare function computeFloatSpeed(cfg: {
|
|
|
15
15
|
speed: number;
|
|
16
16
|
speedRandomness?: number;
|
|
17
17
|
}, seed: string): number;
|
|
18
|
-
|
|
18
|
+
interface GradientStop {
|
|
19
|
+
color: string;
|
|
20
|
+
position: number;
|
|
21
|
+
}
|
|
22
|
+
interface BgInput {
|
|
19
23
|
type: string;
|
|
20
24
|
color?: string;
|
|
21
25
|
gradient?: {
|
|
22
26
|
type: string;
|
|
23
27
|
angle?: number;
|
|
24
28
|
colors: string[];
|
|
29
|
+
stops?: GradientStop[];
|
|
30
|
+
radialShape?: 'circle' | 'ellipse';
|
|
31
|
+
radialPosition?: string;
|
|
25
32
|
};
|
|
26
33
|
image?: string;
|
|
27
|
-
}
|
|
34
|
+
}
|
|
35
|
+
export declare function getBackgroundStyle(bg: BgInput): string;
|
|
36
|
+
export {};
|
package/dist/engine/utils.js
CHANGED
|
@@ -73,16 +73,28 @@ export function computeFloatSpeed(cfg, seed) {
|
|
|
73
73
|
return cfg.speed;
|
|
74
74
|
return cfg.speed * (1 - r + r * hashFraction(seed, 2));
|
|
75
75
|
}
|
|
76
|
+
function buildStops(colors, stops) {
|
|
77
|
+
if (stops && stops.length > 0) {
|
|
78
|
+
const sorted = [...stops].sort((a, b) => a.position - b.position);
|
|
79
|
+
return sorted.map((s) => `${s.color} ${s.position}%`).join(', ');
|
|
80
|
+
}
|
|
81
|
+
if (colors.length <= 1)
|
|
82
|
+
return colors[0] ?? '#000';
|
|
83
|
+
return colors.map((c, i) => `${c} ${(i / (colors.length - 1)) * 100}%`).join(', ');
|
|
84
|
+
}
|
|
76
85
|
export function getBackgroundStyle(bg) {
|
|
77
86
|
if (bg.type === 'transparent')
|
|
78
87
|
return 'background: transparent';
|
|
79
88
|
if (bg.type === 'solid')
|
|
80
89
|
return `background-color: ${bg.color ?? 'transparent'}`;
|
|
81
90
|
if (bg.type === 'gradient' && bg.gradient) {
|
|
82
|
-
const { type, angle = 135, colors } = bg.gradient;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const { type, angle = 135, colors, stops, radialShape = 'circle', radialPosition = 'center' } = bg.gradient;
|
|
92
|
+
const stopList = buildStops(colors, stops);
|
|
93
|
+
if (type === 'conic')
|
|
94
|
+
return `background: conic-gradient(from ${angle}deg at ${radialPosition}, ${stopList})`;
|
|
95
|
+
if (type === 'radial')
|
|
96
|
+
return `background: radial-gradient(${radialShape} at ${radialPosition}, ${stopList})`;
|
|
97
|
+
return `background: linear-gradient(${angle}deg, ${stopList})`;
|
|
86
98
|
}
|
|
87
99
|
if (bg.type === 'image' && bg.image)
|
|
88
100
|
return `background-image: url(${bg.image}); background-size: cover; background-position: center`;
|
package/dist/types.d.ts
CHANGED
|
@@ -107,10 +107,13 @@ export interface CodeElement extends BaseElement {
|
|
|
107
107
|
headerRadius?: number;
|
|
108
108
|
tabRadius?: number;
|
|
109
109
|
}
|
|
110
|
-
export type TextAnimationMode = 'instant' | 'typewriter' | 'fade-words';
|
|
110
|
+
export type TextAnimationMode = 'instant' | 'typewriter' | 'fade-words' | 'fade-letters' | 'handwriting' | 'bounce-in';
|
|
111
111
|
export interface TextAnimationConfig {
|
|
112
112
|
mode: TextAnimationMode;
|
|
113
113
|
typewriterSpeed: number;
|
|
114
|
+
duration?: number;
|
|
115
|
+
stagger?: number;
|
|
116
|
+
loop?: boolean;
|
|
114
117
|
}
|
|
115
118
|
export interface TextElement extends BaseElement {
|
|
116
119
|
type: 'text';
|
|
@@ -316,13 +319,20 @@ export interface ConfettiConfig {
|
|
|
316
319
|
startVelocity: number;
|
|
317
320
|
scalar: number;
|
|
318
321
|
}
|
|
322
|
+
export interface GradientStop {
|
|
323
|
+
color: string;
|
|
324
|
+
position: number;
|
|
325
|
+
}
|
|
319
326
|
export interface CanvasBackground {
|
|
320
327
|
type: 'solid' | 'gradient' | 'image' | 'transparent';
|
|
321
328
|
color?: string;
|
|
322
329
|
gradient?: {
|
|
323
|
-
type: 'linear' | 'radial';
|
|
330
|
+
type: 'linear' | 'radial' | 'conic';
|
|
324
331
|
angle?: number;
|
|
325
332
|
colors: string[];
|
|
333
|
+
stops?: GradientStop[];
|
|
334
|
+
radialShape?: 'circle' | 'ellipse';
|
|
335
|
+
radialPosition?: string;
|
|
326
336
|
};
|
|
327
337
|
image?: string;
|
|
328
338
|
particles?: ParticlesConfig;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: animates a text element's content via JS RAF.
|
|
3
|
+
*
|
|
4
|
+
* Implemented modes:
|
|
5
|
+
* • fade-letters — wraps each character in a <span> and fades them in with stagger
|
|
6
|
+
* • bounce-in — same wrapping, scale+translate pop per character
|
|
7
|
+
* • handwriting — re-renders the text inside an <svg><text> with stroke + transparent
|
|
8
|
+
* fill and animates `stroke-dashoffset` so the text appears to be
|
|
9
|
+
* drawn left-to-right (works best with cursive/script fonts)
|
|
10
|
+
*
|
|
11
|
+
* Other modes (instant, typewriter, fade-words) are handled elsewhere by the
|
|
12
|
+
* /present render path — this action no-ops for those.
|
|
13
|
+
*
|
|
14
|
+
* The action registers a restart fn into `window.__svgAnimRestart` so the
|
|
15
|
+
* server-side video export pipeline can reset all animations under the
|
|
16
|
+
* virtual clock at the start of the slide hold (same pattern as FlowMarkers
|
|
17
|
+
* and arrowClipDraw).
|
|
18
|
+
*/
|
|
19
|
+
export type TextAnimateMode = 'instant' | 'typewriter' | 'fade-words' | 'fade-letters' | 'handwriting' | 'bounce-in';
|
|
20
|
+
export interface TextAnimateParams {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
mode: TextAnimateMode;
|
|
23
|
+
content: string;
|
|
24
|
+
/** Total duration in ms for handwriting; for stagger modes it's the per-letter
|
|
25
|
+
* delay budget — actual total = stagger * letterCount + perLetterDuration. */
|
|
26
|
+
duration: number;
|
|
27
|
+
stagger?: number;
|
|
28
|
+
loop?: boolean;
|
|
29
|
+
/** Cosmetic: applied to the inner SVG text in handwriting mode. */
|
|
30
|
+
color?: string;
|
|
31
|
+
fontSize?: number;
|
|
32
|
+
fontFamily?: string;
|
|
33
|
+
fontWeight?: number | string;
|
|
34
|
+
fontStyle?: string;
|
|
35
|
+
textAlign?: 'left' | 'center' | 'right';
|
|
36
|
+
/** Slide loop duration; when present the effective duration aligns to a
|
|
37
|
+
* cycle that fits an integer number of times into slide_duration so GIF
|
|
38
|
+
* loops are seamless in Flow mode. */
|
|
39
|
+
slideDuration?: number;
|
|
40
|
+
/** Bumped by the host component when ANY meaningful prop changes — without
|
|
41
|
+
* this the action would silently keep running with stale values. */
|
|
42
|
+
key?: unknown;
|
|
43
|
+
}
|
|
44
|
+
export declare function textAnimate(node: HTMLElement, params: TextAnimateParams): {
|
|
45
|
+
update(p: TextAnimateParams): void;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: animates a text element's content via JS RAF.
|
|
3
|
+
*
|
|
4
|
+
* Implemented modes:
|
|
5
|
+
* • fade-letters — wraps each character in a <span> and fades them in with stagger
|
|
6
|
+
* • bounce-in — same wrapping, scale+translate pop per character
|
|
7
|
+
* • handwriting — re-renders the text inside an <svg><text> with stroke + transparent
|
|
8
|
+
* fill and animates `stroke-dashoffset` so the text appears to be
|
|
9
|
+
* drawn left-to-right (works best with cursive/script fonts)
|
|
10
|
+
*
|
|
11
|
+
* Other modes (instant, typewriter, fade-words) are handled elsewhere by the
|
|
12
|
+
* /present render path — this action no-ops for those.
|
|
13
|
+
*
|
|
14
|
+
* The action registers a restart fn into `window.__svgAnimRestart` so the
|
|
15
|
+
* server-side video export pipeline can reset all animations under the
|
|
16
|
+
* virtual clock at the start of the slide hold (same pattern as FlowMarkers
|
|
17
|
+
* and arrowClipDraw).
|
|
18
|
+
*/
|
|
19
|
+
export function textAnimate(node, params) {
|
|
20
|
+
let raf = 0;
|
|
21
|
+
let originalContent = null;
|
|
22
|
+
let activeMode = null;
|
|
23
|
+
function clearAnim() {
|
|
24
|
+
if (raf)
|
|
25
|
+
cancelAnimationFrame(raf);
|
|
26
|
+
raf = 0;
|
|
27
|
+
}
|
|
28
|
+
function restore() {
|
|
29
|
+
clearAnim();
|
|
30
|
+
// Replace whatever we injected with the original content. Doing this on
|
|
31
|
+
// every (re)start keeps the DOM clean when the user toggles modes.
|
|
32
|
+
if (originalContent !== null)
|
|
33
|
+
node.textContent = originalContent;
|
|
34
|
+
}
|
|
35
|
+
function effectiveDuration() {
|
|
36
|
+
const requested = Math.max(50, params.duration || 800);
|
|
37
|
+
if (params.slideDuration && params.slideDuration > 0 && params.loop) {
|
|
38
|
+
return params.slideDuration / Math.max(1, Math.round(params.slideDuration / requested));
|
|
39
|
+
}
|
|
40
|
+
return requested;
|
|
41
|
+
}
|
|
42
|
+
/** Wraps each character of `text` in a <span class="ta-char">. Spaces are
|
|
43
|
+
* also wrapped (with a special class) so the per-letter animation runs
|
|
44
|
+
* uniformly across word boundaries while still preserving spacing. */
|
|
45
|
+
function wrapChars(text) {
|
|
46
|
+
const frag = document.createElement('span');
|
|
47
|
+
frag.className = 'ta-wrap';
|
|
48
|
+
for (const ch of text) {
|
|
49
|
+
const s = document.createElement('span');
|
|
50
|
+
s.className = ch === ' ' ? 'ta-char ta-space' : 'ta-char';
|
|
51
|
+
s.textContent = ch === ' ' ? ' ' : ch; // nbsp so spaces render as fixed-width in inline-block
|
|
52
|
+
s.style.display = 'inline-block';
|
|
53
|
+
frag.appendChild(s);
|
|
54
|
+
}
|
|
55
|
+
return frag;
|
|
56
|
+
}
|
|
57
|
+
function runFadeLetters() {
|
|
58
|
+
// Source the text from params.content, not node.textContent — the host
|
|
59
|
+
// template intentionally renders nothing when an action mode is active
|
|
60
|
+
// (so we don't get a flash of unstyled text), which means the DOM is
|
|
61
|
+
// empty when this runs. Reading textContent here would yield ''.
|
|
62
|
+
originalContent = params.content ?? '';
|
|
63
|
+
node.innerHTML = '';
|
|
64
|
+
const wrap = wrapChars(originalContent);
|
|
65
|
+
node.appendChild(wrap);
|
|
66
|
+
const chars = Array.from(wrap.querySelectorAll('.ta-char'));
|
|
67
|
+
const stagger = params.stagger ?? Math.max(20, Math.min(60, 600 / Math.max(1, chars.length)));
|
|
68
|
+
const perLetter = 280;
|
|
69
|
+
const total = stagger * chars.length + perLetter;
|
|
70
|
+
const dur = effectiveDuration();
|
|
71
|
+
const totalEffective = params.loop ? dur : total;
|
|
72
|
+
for (const c of chars)
|
|
73
|
+
c.style.opacity = '0';
|
|
74
|
+
const start = performance.now();
|
|
75
|
+
function step(now) {
|
|
76
|
+
const elapsed = (now - start) % (params.loop ? totalEffective : Number.POSITIVE_INFINITY);
|
|
77
|
+
let allDone = true;
|
|
78
|
+
for (let i = 0; i < chars.length; i++) {
|
|
79
|
+
const localStart = i * stagger;
|
|
80
|
+
const t = Math.min(1, Math.max(0, (elapsed - localStart) / perLetter));
|
|
81
|
+
if (t < 1)
|
|
82
|
+
allDone = false;
|
|
83
|
+
chars[i].style.opacity = String(t);
|
|
84
|
+
}
|
|
85
|
+
if (params.loop || !allDone)
|
|
86
|
+
raf = requestAnimationFrame(step);
|
|
87
|
+
else
|
|
88
|
+
raf = 0;
|
|
89
|
+
}
|
|
90
|
+
raf = requestAnimationFrame(step);
|
|
91
|
+
}
|
|
92
|
+
function runBounceIn() {
|
|
93
|
+
// Source the text from params.content, not node.textContent — the host
|
|
94
|
+
// template intentionally renders nothing when an action mode is active
|
|
95
|
+
// (so we don't get a flash of unstyled text), which means the DOM is
|
|
96
|
+
// empty when this runs. Reading textContent here would yield ''.
|
|
97
|
+
originalContent = params.content ?? '';
|
|
98
|
+
node.innerHTML = '';
|
|
99
|
+
const wrap = wrapChars(originalContent);
|
|
100
|
+
node.appendChild(wrap);
|
|
101
|
+
const chars = Array.from(wrap.querySelectorAll('.ta-char'));
|
|
102
|
+
const stagger = params.stagger ?? Math.max(30, Math.min(80, 800 / Math.max(1, chars.length)));
|
|
103
|
+
const perLetter = 380;
|
|
104
|
+
for (const c of chars) {
|
|
105
|
+
c.style.opacity = '0';
|
|
106
|
+
c.style.transform = 'translateY(0.4em) scale(0.6)';
|
|
107
|
+
c.style.transformOrigin = '50% 80%';
|
|
108
|
+
}
|
|
109
|
+
// `let` because the loop branch resets `start = now` to begin a new cycle.
|
|
110
|
+
let start = performance.now();
|
|
111
|
+
function step(now) {
|
|
112
|
+
const elapsed = now - start;
|
|
113
|
+
let allDone = true;
|
|
114
|
+
for (let i = 0; i < chars.length; i++) {
|
|
115
|
+
const t = Math.min(1, Math.max(0, (elapsed - i * stagger) / perLetter));
|
|
116
|
+
if (t < 1)
|
|
117
|
+
allDone = false;
|
|
118
|
+
// Spring-ish easing — overshoots slightly then settles.
|
|
119
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
120
|
+
const overshoot = Math.sin(t * Math.PI) * 0.1;
|
|
121
|
+
const scale = 0.6 + (1 - 0.6) * eased + overshoot;
|
|
122
|
+
const translate = (1 - eased) * 0.4;
|
|
123
|
+
chars[i].style.opacity = String(eased);
|
|
124
|
+
chars[i].style.transform = `translateY(${translate}em) scale(${scale})`;
|
|
125
|
+
}
|
|
126
|
+
if (!allDone)
|
|
127
|
+
raf = requestAnimationFrame(step);
|
|
128
|
+
else if (params.loop) {
|
|
129
|
+
// Reset and loop — a small pause at the end keeps it readable.
|
|
130
|
+
const totalCycle = effectiveDuration();
|
|
131
|
+
if (now - start >= totalCycle) {
|
|
132
|
+
for (const c of chars) {
|
|
133
|
+
c.style.opacity = '0';
|
|
134
|
+
c.style.transform = 'translateY(0.4em) scale(0.6)';
|
|
135
|
+
}
|
|
136
|
+
start = now;
|
|
137
|
+
}
|
|
138
|
+
raf = requestAnimationFrame(step);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
raf = 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
raf = requestAnimationFrame(step);
|
|
145
|
+
}
|
|
146
|
+
function runHandwriting() {
|
|
147
|
+
// Real per-character handwriting: each glyph is its own <text> element
|
|
148
|
+
// with its own stroke-dasharray. We measure char positions from a single
|
|
149
|
+
// hidden <text> first (the only reliable way to get kerning/spacing
|
|
150
|
+
// matching what the browser would draw) then build per-glyph elements
|
|
151
|
+
// laid out in absolute SVG coordinates. Each glyph is revealed in turn
|
|
152
|
+
// with a small overlap so the effect reads like a continuous pen, not
|
|
153
|
+
// a typewriter.
|
|
154
|
+
originalContent = params.content ?? '';
|
|
155
|
+
node.innerHTML = '';
|
|
156
|
+
if (!originalContent)
|
|
157
|
+
return;
|
|
158
|
+
const color = params.color ?? '#ffffff';
|
|
159
|
+
const fs = params.fontSize ?? 48;
|
|
160
|
+
const ff = params.fontFamily ?? 'inherit';
|
|
161
|
+
const fw = params.fontWeight ?? 400;
|
|
162
|
+
const fst = params.fontStyle ?? 'normal';
|
|
163
|
+
const align = params.textAlign ?? 'left';
|
|
164
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
165
|
+
const svg = document.createElementNS(svgNS, 'svg');
|
|
166
|
+
svg.setAttribute('width', '100%');
|
|
167
|
+
svg.setAttribute('height', '100%');
|
|
168
|
+
const w = Math.max(1, node.clientWidth);
|
|
169
|
+
const h = Math.max(1, node.clientHeight);
|
|
170
|
+
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
171
|
+
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|
172
|
+
svg.style.overflow = 'visible';
|
|
173
|
+
node.appendChild(svg);
|
|
174
|
+
// Step 1 — measure char positions from a hidden full-string render.
|
|
175
|
+
const measure = document.createElementNS(svgNS, 'text');
|
|
176
|
+
measure.setAttribute('x', '0');
|
|
177
|
+
measure.setAttribute('y', '50%');
|
|
178
|
+
measure.setAttribute('dominant-baseline', 'middle');
|
|
179
|
+
measure.setAttribute('text-anchor', 'start');
|
|
180
|
+
measure.setAttribute('visibility', 'hidden');
|
|
181
|
+
measure.style.fontSize = `${fs}px`;
|
|
182
|
+
measure.style.fontFamily = ff;
|
|
183
|
+
measure.style.fontWeight = String(fw);
|
|
184
|
+
measure.style.fontStyle = fst;
|
|
185
|
+
measure.textContent = originalContent;
|
|
186
|
+
svg.appendChild(measure);
|
|
187
|
+
const charPositions = [];
|
|
188
|
+
const charLengths = [];
|
|
189
|
+
let totalWidth = 0;
|
|
190
|
+
try {
|
|
191
|
+
const t = measure;
|
|
192
|
+
totalWidth = t.getComputedTextLength();
|
|
193
|
+
for (let i = 0; i < originalContent.length; i++) {
|
|
194
|
+
const pt = t.getStartPositionOfChar(i);
|
|
195
|
+
charPositions.push(pt.x);
|
|
196
|
+
const nextX = i + 1 < originalContent.length
|
|
197
|
+
? t.getStartPositionOfChar(i + 1).x
|
|
198
|
+
: pt.x + (totalWidth - pt.x);
|
|
199
|
+
charLengths.push(Math.max(1, nextX - pt.x));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Fallback: even spacing across host width.
|
|
204
|
+
totalWidth = w * 0.9;
|
|
205
|
+
const stepW = totalWidth / Math.max(1, originalContent.length);
|
|
206
|
+
for (let i = 0; i < originalContent.length; i++) {
|
|
207
|
+
charPositions.push(i * stepW);
|
|
208
|
+
charLengths.push(stepW);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Honor textAlign by shifting the whole row in absolute coordinates.
|
|
212
|
+
const baseX = align === 'center' ? (w - totalWidth) / 2
|
|
213
|
+
: align === 'right' ? (w - totalWidth)
|
|
214
|
+
: 0;
|
|
215
|
+
svg.removeChild(measure);
|
|
216
|
+
// Step 2 — build per-character <text> elements.
|
|
217
|
+
const glyphs = [];
|
|
218
|
+
for (let i = 0; i < originalContent.length; i++) {
|
|
219
|
+
const ch = originalContent[i];
|
|
220
|
+
const t = document.createElementNS(svgNS, 'text');
|
|
221
|
+
t.setAttribute('x', String(baseX + charPositions[i]));
|
|
222
|
+
t.setAttribute('y', '50%');
|
|
223
|
+
t.setAttribute('dominant-baseline', 'middle');
|
|
224
|
+
t.setAttribute('text-anchor', 'start');
|
|
225
|
+
t.setAttribute('fill', 'transparent');
|
|
226
|
+
t.setAttribute('stroke', color);
|
|
227
|
+
t.setAttribute('stroke-width', '1');
|
|
228
|
+
t.setAttribute('stroke-linecap', 'round');
|
|
229
|
+
t.setAttribute('stroke-linejoin', 'round');
|
|
230
|
+
t.style.fontSize = `${fs}px`;
|
|
231
|
+
t.style.fontFamily = ff;
|
|
232
|
+
t.style.fontWeight = String(fw);
|
|
233
|
+
t.style.fontStyle = fst;
|
|
234
|
+
t.textContent = ch;
|
|
235
|
+
svg.appendChild(t);
|
|
236
|
+
// Spaces have no visible stroke — skip dashoffset math, they reveal
|
|
237
|
+
// instantly. For everything else, we don't know the exact outline
|
|
238
|
+
// length per glyph, so we pick a generous upper bound that comfortably
|
|
239
|
+
// exceeds any realistic glyph contour at this font size, then use an
|
|
240
|
+
// explicit *huge* gap so the dash pattern can't repeat. Without the
|
|
241
|
+
// huge gap, complex letters (g, B, &, ampersand-heavy scripts) whose
|
|
242
|
+
// outline is longer than our `len` estimate would show a second dash
|
|
243
|
+
// cycle peeking through before the animation reaches them.
|
|
244
|
+
const len = ch === ' ' ? 0 : Math.max(40, fs * 4 + charLengths[i] * 2.5);
|
|
245
|
+
const gap = len * 4;
|
|
246
|
+
t.style.strokeDasharray = `${len || 1} ${gap || 1}`;
|
|
247
|
+
t.style.strokeDashoffset = String(len || 1);
|
|
248
|
+
glyphs.push({ el: t, len });
|
|
249
|
+
}
|
|
250
|
+
const dur = effectiveDuration();
|
|
251
|
+
// Per-char duration with overlap. overlap=0.6 means each char starts after
|
|
252
|
+
// the previous is 60% drawn — looks like a continuous pen rather than a
|
|
253
|
+
// typewriter (overlap=1) or strict per-letter sequence (overlap=0).
|
|
254
|
+
const n = originalContent.length;
|
|
255
|
+
const overlap = 0.6;
|
|
256
|
+
// Solve: stagger * (n - 1) + perChar = dur, where stagger = perChar * overlap.
|
|
257
|
+
// → perChar * (overlap * (n - 1) + 1) = dur.
|
|
258
|
+
const perChar = dur / Math.max(1, overlap * (n - 1) + 1);
|
|
259
|
+
const stagger = perChar * overlap;
|
|
260
|
+
let start = performance.now();
|
|
261
|
+
function step(now) {
|
|
262
|
+
const elapsed = now - start;
|
|
263
|
+
let allDone = true;
|
|
264
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
265
|
+
const g = glyphs[i];
|
|
266
|
+
if (g.len === 0)
|
|
267
|
+
continue; // space — already invisible
|
|
268
|
+
const localStart = i * stagger;
|
|
269
|
+
const t = Math.min(1, Math.max(0, (elapsed - localStart) / perChar));
|
|
270
|
+
if (t < 1)
|
|
271
|
+
allDone = false;
|
|
272
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
273
|
+
g.el.style.strokeDashoffset = String(g.len * (1 - eased));
|
|
274
|
+
// Cross-fade fill in over the last 30% of the glyph's draw so the
|
|
275
|
+
// finished letter reads cleanly. Otherwise thin script fonts look
|
|
276
|
+
// hollow and don't match the rest of the slide's text.
|
|
277
|
+
if (t > 0.7) {
|
|
278
|
+
const fillT = (t - 0.7) / 0.3;
|
|
279
|
+
g.el.setAttribute('fill', color);
|
|
280
|
+
g.el.setAttribute('fill-opacity', String(fillT));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!allDone) {
|
|
284
|
+
raf = requestAnimationFrame(step);
|
|
285
|
+
}
|
|
286
|
+
else if (params.loop) {
|
|
287
|
+
for (const g of glyphs) {
|
|
288
|
+
g.el.style.strokeDashoffset = String(g.len || 1);
|
|
289
|
+
g.el.setAttribute('fill', 'transparent');
|
|
290
|
+
g.el.removeAttribute('fill-opacity');
|
|
291
|
+
}
|
|
292
|
+
start = now;
|
|
293
|
+
raf = requestAnimationFrame(step);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
raf = 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
raf = requestAnimationFrame(step);
|
|
300
|
+
}
|
|
301
|
+
function run() {
|
|
302
|
+
restore();
|
|
303
|
+
activeMode = params.mode;
|
|
304
|
+
if (!params.enabled)
|
|
305
|
+
return;
|
|
306
|
+
switch (params.mode) {
|
|
307
|
+
case 'fade-letters':
|
|
308
|
+
runFadeLetters();
|
|
309
|
+
break;
|
|
310
|
+
case 'bounce-in':
|
|
311
|
+
runBounceIn();
|
|
312
|
+
break;
|
|
313
|
+
case 'handwriting':
|
|
314
|
+
runHandwriting();
|
|
315
|
+
break;
|
|
316
|
+
default: /* other modes handled elsewhere */ break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
queueMicrotask(run);
|
|
320
|
+
if (typeof window !== 'undefined') {
|
|
321
|
+
const reg = (window.__svgAnimRestart ||= []);
|
|
322
|
+
reg.push(run);
|
|
323
|
+
node.__svgAnimRestart = run;
|
|
324
|
+
}
|
|
325
|
+
let lastKey = params.key;
|
|
326
|
+
return {
|
|
327
|
+
update(p) {
|
|
328
|
+
const keyChanged = p.key !== lastKey || p.mode !== activeMode;
|
|
329
|
+
params = p;
|
|
330
|
+
if (keyChanged) {
|
|
331
|
+
lastKey = p.key;
|
|
332
|
+
queueMicrotask(run);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
destroy() {
|
|
336
|
+
restore();
|
|
337
|
+
if (typeof window !== 'undefined') {
|
|
338
|
+
const reg = window.__svgAnimRestart;
|
|
339
|
+
const fn = node.__svgAnimRestart;
|
|
340
|
+
if (reg && fn) {
|
|
341
|
+
const idx = reg.indexOf(fn);
|
|
342
|
+
if (idx >= 0)
|
|
343
|
+
reg.splice(idx, 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "animot-presenter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Embed animated presentations anywhere. Works with vanilla JS, React, Vue, Angular, Svelte, and any frontend framework. Morphing animations, code highlighting, charts, particles, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|