@mihirsarya/manim-scroll-runtime 0.2.0 → 0.2.2
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/README.md +231 -0
- package/dist/index.d.ts +1 -1
- package/dist/native-player.d.ts +54 -1
- package/dist/native-player.js +193 -6
- package/dist/types.d.ts +25 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @mihirsarya/manim-scroll-runtime
|
|
2
|
+
|
|
3
|
+
Core scroll-driven playback runtime for pre-rendered Manim animations. Works in any JavaScript environment—use directly in vanilla JS or as the foundation for framework integrations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mihirsarya/manim-scroll-runtime
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use the unified package (recommended for React/Next.js):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @mihirsarya/manim-scroll
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Video or frame-by-frame playback** with automatic fallback
|
|
20
|
+
- **Flexible scroll ranges** via presets, relative units, or pixels
|
|
21
|
+
- **Native text animation** using SVG (no pre-rendered assets needed)
|
|
22
|
+
- **Zero framework dependencies** for the core runtime
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Pre-rendered Animation (Video/Frames)
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { registerScrollAnimation } from "@mihirsarya/manim-scroll-runtime";
|
|
30
|
+
|
|
31
|
+
const container = document.querySelector("#hero") as HTMLElement;
|
|
32
|
+
|
|
33
|
+
const cleanup = await registerScrollAnimation({
|
|
34
|
+
container,
|
|
35
|
+
manifestUrl: "/assets/scene/manifest.json",
|
|
36
|
+
scrollRange: "viewport",
|
|
37
|
+
onReady: () => console.log("Animation ready"),
|
|
38
|
+
onProgress: (progress) => console.log(`Progress: ${progress}`),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Call cleanup() when done to remove listeners
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Native Text Animation
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { registerNativeAnimation } from "@mihirsarya/manim-scroll-runtime";
|
|
48
|
+
|
|
49
|
+
const container = document.querySelector("#text") as HTMLElement;
|
|
50
|
+
|
|
51
|
+
const cleanup = await registerNativeAnimation({
|
|
52
|
+
container,
|
|
53
|
+
text: "Hello World",
|
|
54
|
+
fontSize: 48,
|
|
55
|
+
color: "#ffffff",
|
|
56
|
+
scrollRange: "viewport",
|
|
57
|
+
onReady: () => console.log("Ready"),
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Exports
|
|
62
|
+
|
|
63
|
+
### Functions
|
|
64
|
+
|
|
65
|
+
- **`registerScrollAnimation(options)`** - Register a scroll-driven animation with pre-rendered assets
|
|
66
|
+
- **`registerNativeAnimation(options)`** - Register a native SVG text animation
|
|
67
|
+
|
|
68
|
+
### Classes
|
|
69
|
+
|
|
70
|
+
- **`NativeTextPlayer`** - Low-level native text animation player
|
|
71
|
+
|
|
72
|
+
### Types
|
|
73
|
+
|
|
74
|
+
- `RenderManifest` - Animation manifest schema
|
|
75
|
+
- `ScrollAnimationOptions` - Options for `registerScrollAnimation`
|
|
76
|
+
- `NativeAnimationOptions` - Options for `registerNativeAnimation`
|
|
77
|
+
- `ScrollRange`, `ScrollRangePreset`, `ScrollRangeValue` - Scroll range types
|
|
78
|
+
|
|
79
|
+
## API Reference
|
|
80
|
+
|
|
81
|
+
### registerScrollAnimation(options)
|
|
82
|
+
|
|
83
|
+
Registers a scroll-driven animation using pre-rendered video or frame assets.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
interface ScrollAnimationOptions {
|
|
87
|
+
/** Container element for the animation */
|
|
88
|
+
container: HTMLElement;
|
|
89
|
+
/** URL to the animation manifest.json */
|
|
90
|
+
manifestUrl: string;
|
|
91
|
+
/** Playback mode (default: "auto") */
|
|
92
|
+
mode?: "auto" | "frames" | "video";
|
|
93
|
+
/** Optional canvas element (created automatically if not provided) */
|
|
94
|
+
canvas?: HTMLCanvasElement;
|
|
95
|
+
/** Scroll range configuration */
|
|
96
|
+
scrollRange?: ScrollRangeValue;
|
|
97
|
+
/** Called when animation is loaded and ready */
|
|
98
|
+
onReady?: () => void;
|
|
99
|
+
/** Called on scroll progress updates (0 to 1) */
|
|
100
|
+
onProgress?: (progress: number) => void;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Returns:** `Promise<() => void>` - Cleanup function to remove listeners
|
|
105
|
+
|
|
106
|
+
### registerNativeAnimation(options)
|
|
107
|
+
|
|
108
|
+
Registers a native text animation that renders in the browser using SVG, replicating Manim's Write/DrawBorderThenFill effect.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
interface NativeAnimationOptions {
|
|
112
|
+
/** Container element for the animation */
|
|
113
|
+
container: HTMLElement;
|
|
114
|
+
/** Text to animate */
|
|
115
|
+
text: string;
|
|
116
|
+
/** Font size in pixels (inherits from parent if not specified) */
|
|
117
|
+
fontSize?: number;
|
|
118
|
+
/** Text color (hex or CSS color) */
|
|
119
|
+
color?: string;
|
|
120
|
+
/** URL to a font file (woff, woff2, ttf, otf) */
|
|
121
|
+
fontUrl?: string;
|
|
122
|
+
/** Stroke width for the drawing phase (default: 2) */
|
|
123
|
+
strokeWidth?: number;
|
|
124
|
+
/** Scroll range configuration */
|
|
125
|
+
scrollRange?: ScrollRangeValue;
|
|
126
|
+
/** Called when animation is loaded and ready */
|
|
127
|
+
onReady?: () => void;
|
|
128
|
+
/** Called on scroll progress updates (0 to 1) */
|
|
129
|
+
onProgress?: (progress: number) => void;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Returns:** `Promise<() => void>` - Cleanup function to remove listeners
|
|
134
|
+
|
|
135
|
+
## Scroll Range Configuration
|
|
136
|
+
|
|
137
|
+
The `scrollRange` option controls when the animation plays relative to scroll position.
|
|
138
|
+
|
|
139
|
+
### Presets
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// Animation plays as element crosses the viewport (most common)
|
|
143
|
+
scrollRange: "viewport"
|
|
144
|
+
|
|
145
|
+
// Animation tied to element's own scroll position
|
|
146
|
+
scrollRange: "element"
|
|
147
|
+
|
|
148
|
+
// Animation spans entire document scroll
|
|
149
|
+
scrollRange: "full"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Relative Units
|
|
153
|
+
|
|
154
|
+
Use viewport height (`vh`) or element percentage (`%`):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// Start when element is 100vh from top, end at -50% of element height
|
|
158
|
+
scrollRange: ["100vh", "-50%"]
|
|
159
|
+
|
|
160
|
+
// Mix units as needed
|
|
161
|
+
scrollRange: ["80vh", "-100%"]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Pixel Values
|
|
165
|
+
|
|
166
|
+
For precise control:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// As a tuple
|
|
170
|
+
scrollRange: [800, -400]
|
|
171
|
+
|
|
172
|
+
// As an object (legacy format)
|
|
173
|
+
scrollRange: { start: 800, end: -400 }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Manifest Schema
|
|
177
|
+
|
|
178
|
+
The `manifest.json` file describes a pre-rendered animation:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
interface RenderManifest {
|
|
182
|
+
/** Scene name */
|
|
183
|
+
scene: string;
|
|
184
|
+
/** Frames per second */
|
|
185
|
+
fps: number;
|
|
186
|
+
/** Canvas width */
|
|
187
|
+
width: number;
|
|
188
|
+
/** Canvas height */
|
|
189
|
+
height: number;
|
|
190
|
+
/** Array of frame image URLs */
|
|
191
|
+
frames: string[];
|
|
192
|
+
/** Video URL (null if not available) */
|
|
193
|
+
video: string | null;
|
|
194
|
+
/** Whether rendered with transparent background */
|
|
195
|
+
transparent?: boolean;
|
|
196
|
+
/** Whether this is an inline animation */
|
|
197
|
+
inline?: boolean;
|
|
198
|
+
/** Aspect ratio for inline sizing */
|
|
199
|
+
aspectRatio?: number | null;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Playback Modes
|
|
204
|
+
|
|
205
|
+
| Mode | Description |
|
|
206
|
+
|------|-------------|
|
|
207
|
+
| `"auto"` | Uses video if available, falls back to frames |
|
|
208
|
+
| `"video"` | Forces video playback (fails if no video) |
|
|
209
|
+
| `"frames"` | Forces frame-by-frame playback |
|
|
210
|
+
|
|
211
|
+
## Browser Usage (CDN)
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
<script type="module">
|
|
215
|
+
import { registerScrollAnimation } from "https://esm.sh/@mihirsarya/manim-scroll-runtime";
|
|
216
|
+
|
|
217
|
+
registerScrollAnimation({
|
|
218
|
+
container: document.querySelector("#hero"),
|
|
219
|
+
manifestUrl: "/assets/scene/manifest.json",
|
|
220
|
+
scrollRange: "viewport",
|
|
221
|
+
});
|
|
222
|
+
</script>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Dependencies
|
|
226
|
+
|
|
227
|
+
- [opentype.js](https://opentype.js.org/) - For native text animation font parsing
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NativeTextPlayer, registerNativeAnimation } from "./native-player";
|
|
2
2
|
import type { ScrollAnimationOptions } from "./types";
|
|
3
|
-
export type { RenderManifest, ScrollAnimationOptions, ScrollRange, ScrollRangePreset, ScrollRangeValue, NativeAnimationOptions, } from "./types";
|
|
3
|
+
export type { RenderManifest, ScrollAnimationOptions, ScrollRange, ScrollRangePreset, ScrollRangeValue, NativeAnimationOptions, EasingFunction, EasingPreset, PlaybackOptions, } from "./types";
|
|
4
4
|
export { NativeTextPlayer, registerNativeAnimation };
|
|
5
5
|
export declare function registerScrollAnimation(options: ScrollAnimationOptions): Promise<() => void>;
|
package/dist/native-player.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NativeAnimationOptions } from "./types";
|
|
1
|
+
import type { NativeAnimationOptions, PlaybackOptions } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* NativeTextPlayer - Renders text animation natively in the browser
|
|
4
4
|
* using SVG paths, replicating Manim's Write/DrawBorderThenFill animation.
|
|
@@ -32,8 +32,61 @@ export declare class NativeTextPlayer {
|
|
|
32
32
|
private font;
|
|
33
33
|
/** Last known font size, used to detect changes for inherited sizing */
|
|
34
34
|
private lastComputedFontSize;
|
|
35
|
+
/** Whether time-based animation is currently playing */
|
|
36
|
+
private _isPlaying;
|
|
37
|
+
/** RAF ID for playback animation loop */
|
|
38
|
+
private playbackRafId;
|
|
39
|
+
/** Start time of the current playback (from performance.now()) */
|
|
40
|
+
private playbackStartTime;
|
|
41
|
+
/** Duration of current playback in milliseconds */
|
|
42
|
+
private playbackDuration;
|
|
43
|
+
/** Easing function for current playback */
|
|
44
|
+
private playbackEasing;
|
|
45
|
+
/** Direction of playback: 1 for forward, -1 for reverse */
|
|
46
|
+
private playbackDirection;
|
|
47
|
+
/** Whether to loop the playback */
|
|
48
|
+
private playbackLoop;
|
|
49
|
+
/** Callback when playback completes */
|
|
50
|
+
private playbackOnComplete?;
|
|
51
|
+
/** Current progress value (0 to 1) - used in controlled mode */
|
|
52
|
+
private currentProgress;
|
|
53
|
+
/** Whether we're in controlled/programmatic mode (no scroll binding) */
|
|
54
|
+
private isControlledMode;
|
|
35
55
|
constructor(options: NativeAnimationOptions);
|
|
36
56
|
init(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the animation is currently playing (time-based).
|
|
59
|
+
*/
|
|
60
|
+
get isPlaying(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Get the current progress value (0 to 1).
|
|
63
|
+
*/
|
|
64
|
+
get progress(): number;
|
|
65
|
+
/**
|
|
66
|
+
* Set the animation progress directly (0 to 1).
|
|
67
|
+
* This is the primary method for controlled/declarative usage.
|
|
68
|
+
* Stops any ongoing playback.
|
|
69
|
+
*/
|
|
70
|
+
setProgress(progress: number): void;
|
|
71
|
+
/**
|
|
72
|
+
* Play the animation over a specified duration.
|
|
73
|
+
* @param options - Playback options (duration, easing, loop, etc.)
|
|
74
|
+
* or just duration in milliseconds
|
|
75
|
+
*/
|
|
76
|
+
play(options?: PlaybackOptions | number): void;
|
|
77
|
+
/**
|
|
78
|
+
* Pause the animation playback.
|
|
79
|
+
*/
|
|
80
|
+
pause(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Seek to a specific progress value (0 to 1).
|
|
83
|
+
* Unlike setProgress, this doesn't affect the playing state.
|
|
84
|
+
* If playing, the animation will continue from the new position.
|
|
85
|
+
*/
|
|
86
|
+
seek(progress: number): void;
|
|
87
|
+
private startPlayback;
|
|
88
|
+
private pausePlayback;
|
|
89
|
+
private playbackTick;
|
|
37
90
|
private createCharacterPaths;
|
|
38
91
|
/**
|
|
39
92
|
* Get the inherited font size from the container's computed style.
|
package/dist/native-player.js
CHANGED
|
@@ -72,6 +72,36 @@ function integerInterpolate(start, end, alpha) {
|
|
|
72
72
|
const subalpha = scaledAlpha - index;
|
|
73
73
|
return [start + index, subalpha];
|
|
74
74
|
}
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Easing Functions for Playback
|
|
77
|
+
// ============================================================================
|
|
78
|
+
/** Standard ease-in (quadratic) */
|
|
79
|
+
function easeIn(t) {
|
|
80
|
+
return t * t;
|
|
81
|
+
}
|
|
82
|
+
/** Standard ease-out (quadratic) */
|
|
83
|
+
function easeOut(t) {
|
|
84
|
+
return 1 - (1 - t) * (1 - t);
|
|
85
|
+
}
|
|
86
|
+
/** Standard ease-in-out (quadratic) */
|
|
87
|
+
function easeInOut(t) {
|
|
88
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
89
|
+
}
|
|
90
|
+
/** Resolve an easing preset to an easing function */
|
|
91
|
+
function resolveEasing(easing) {
|
|
92
|
+
if (typeof easing === "function") {
|
|
93
|
+
return easing;
|
|
94
|
+
}
|
|
95
|
+
switch (easing) {
|
|
96
|
+
case "ease-in": return easeIn;
|
|
97
|
+
case "ease-out": return easeOut;
|
|
98
|
+
case "ease-in-out": return easeInOut;
|
|
99
|
+
case "smooth": return smooth;
|
|
100
|
+
case "linear":
|
|
101
|
+
default:
|
|
102
|
+
return linear;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
75
105
|
/**
|
|
76
106
|
* Parse a relative unit string (e.g., "100vh", "-50%") to pixels.
|
|
77
107
|
*/
|
|
@@ -486,6 +516,7 @@ function splitPathIntoSegments(pathData) {
|
|
|
486
516
|
*/
|
|
487
517
|
export class NativeTextPlayer {
|
|
488
518
|
constructor(options) {
|
|
519
|
+
var _a;
|
|
489
520
|
this.svg = null;
|
|
490
521
|
this.fallbackWrapper = null;
|
|
491
522
|
/** All sub-paths (segments) across all characters, for stroke animation */
|
|
@@ -500,6 +531,57 @@ export class NativeTextPlayer {
|
|
|
500
531
|
this.font = null;
|
|
501
532
|
/** Last known font size, used to detect changes for inherited sizing */
|
|
502
533
|
this.lastComputedFontSize = 0;
|
|
534
|
+
// ==================== Playback State ====================
|
|
535
|
+
/** Whether time-based animation is currently playing */
|
|
536
|
+
this._isPlaying = false;
|
|
537
|
+
/** RAF ID for playback animation loop */
|
|
538
|
+
this.playbackRafId = null;
|
|
539
|
+
/** Start time of the current playback (from performance.now()) */
|
|
540
|
+
this.playbackStartTime = 0;
|
|
541
|
+
/** Duration of current playback in milliseconds */
|
|
542
|
+
this.playbackDuration = 0;
|
|
543
|
+
/** Easing function for current playback */
|
|
544
|
+
this.playbackEasing = linear;
|
|
545
|
+
/** Direction of playback: 1 for forward, -1 for reverse */
|
|
546
|
+
this.playbackDirection = 1;
|
|
547
|
+
/** Whether to loop the playback */
|
|
548
|
+
this.playbackLoop = false;
|
|
549
|
+
/** Current progress value (0 to 1) - used in controlled mode */
|
|
550
|
+
this.currentProgress = 0;
|
|
551
|
+
/** Whether we're in controlled/programmatic mode (no scroll binding) */
|
|
552
|
+
this.isControlledMode = false;
|
|
553
|
+
this.playbackTick = () => {
|
|
554
|
+
var _a, _b, _c;
|
|
555
|
+
if (!this._isPlaying)
|
|
556
|
+
return;
|
|
557
|
+
const elapsed = performance.now() - this.playbackStartTime;
|
|
558
|
+
let rawProgress = elapsed / this.playbackDuration;
|
|
559
|
+
if (rawProgress >= 1) {
|
|
560
|
+
if (this.playbackLoop) {
|
|
561
|
+
// Loop: reset and continue
|
|
562
|
+
this.playbackStartTime = performance.now();
|
|
563
|
+
rawProgress = 0;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
// Complete
|
|
567
|
+
rawProgress = 1;
|
|
568
|
+
this._isPlaying = false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// Apply easing and direction
|
|
572
|
+
const easedProgress = this.playbackEasing(rawProgress);
|
|
573
|
+
this.currentProgress = this.playbackDirection === 1
|
|
574
|
+
? easedProgress
|
|
575
|
+
: 1 - easedProgress;
|
|
576
|
+
this.render(this.currentProgress);
|
|
577
|
+
(_b = (_a = this.options).onProgress) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentProgress);
|
|
578
|
+
if (this._isPlaying) {
|
|
579
|
+
this.playbackRafId = requestAnimationFrame(this.playbackTick);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
(_c = this.playbackOnComplete) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
503
585
|
// Manim defaults: DrawBorderThenFill stroke_width = 2
|
|
504
586
|
// fontSize: undefined means inherit from parent element
|
|
505
587
|
this.options = {
|
|
@@ -508,6 +590,9 @@ export class NativeTextPlayer {
|
|
|
508
590
|
...options,
|
|
509
591
|
};
|
|
510
592
|
this.container = options.container;
|
|
593
|
+
// If progress is provided, we're in controlled mode
|
|
594
|
+
this.isControlledMode = options.progress !== undefined;
|
|
595
|
+
this.currentProgress = (_a = options.progress) !== null && _a !== void 0 ? _a : 0;
|
|
511
596
|
}
|
|
512
597
|
async init() {
|
|
513
598
|
var _a, _b;
|
|
@@ -535,14 +620,115 @@ export class NativeTextPlayer {
|
|
|
535
620
|
if (this.svg) {
|
|
536
621
|
this.container.appendChild(this.svg);
|
|
537
622
|
}
|
|
538
|
-
//
|
|
539
|
-
this.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
623
|
+
// Only setup scroll-based animation if not in controlled mode
|
|
624
|
+
if (!this.isControlledMode) {
|
|
625
|
+
// Setup intersection observer
|
|
626
|
+
this.setupObserver();
|
|
627
|
+
// Setup resize handling for responsiveness
|
|
628
|
+
this.setupResizeHandling();
|
|
629
|
+
}
|
|
630
|
+
// Draw initial state
|
|
631
|
+
this.render(this.currentProgress);
|
|
544
632
|
(_b = (_a = this.options).onReady) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
545
633
|
}
|
|
634
|
+
// ==================== Public Playback API ====================
|
|
635
|
+
/**
|
|
636
|
+
* Whether the animation is currently playing (time-based).
|
|
637
|
+
*/
|
|
638
|
+
get isPlaying() {
|
|
639
|
+
return this._isPlaying;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Get the current progress value (0 to 1).
|
|
643
|
+
*/
|
|
644
|
+
get progress() {
|
|
645
|
+
return this.currentProgress;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Set the animation progress directly (0 to 1).
|
|
649
|
+
* This is the primary method for controlled/declarative usage.
|
|
650
|
+
* Stops any ongoing playback.
|
|
651
|
+
*/
|
|
652
|
+
setProgress(progress) {
|
|
653
|
+
var _a, _b;
|
|
654
|
+
this.pausePlayback();
|
|
655
|
+
this.currentProgress = clamp(progress, 0, 1);
|
|
656
|
+
this.render(this.currentProgress);
|
|
657
|
+
(_b = (_a = this.options).onProgress) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentProgress);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Play the animation over a specified duration.
|
|
661
|
+
* @param options - Playback options (duration, easing, loop, etc.)
|
|
662
|
+
* or just duration in milliseconds
|
|
663
|
+
*/
|
|
664
|
+
play(options) {
|
|
665
|
+
var _a, _b, _c, _d;
|
|
666
|
+
this.pausePlayback();
|
|
667
|
+
// Parse options
|
|
668
|
+
const opts = typeof options === "number"
|
|
669
|
+
? { duration: options }
|
|
670
|
+
: (options !== null && options !== void 0 ? options : {});
|
|
671
|
+
const duration = (_a = opts.duration) !== null && _a !== void 0 ? _a : 1000;
|
|
672
|
+
const delay = (_b = opts.delay) !== null && _b !== void 0 ? _b : 0;
|
|
673
|
+
this.playbackDuration = duration;
|
|
674
|
+
this.playbackEasing = resolveEasing(opts.easing);
|
|
675
|
+
this.playbackDirection = (_c = opts.direction) !== null && _c !== void 0 ? _c : 1;
|
|
676
|
+
this.playbackLoop = (_d = opts.loop) !== null && _d !== void 0 ? _d : false;
|
|
677
|
+
this.playbackOnComplete = opts.onComplete;
|
|
678
|
+
// Set initial progress based on direction
|
|
679
|
+
if (this.playbackDirection === 1 && this.currentProgress >= 1) {
|
|
680
|
+
this.currentProgress = 0;
|
|
681
|
+
}
|
|
682
|
+
else if (this.playbackDirection === -1 && this.currentProgress <= 0) {
|
|
683
|
+
this.currentProgress = 1;
|
|
684
|
+
}
|
|
685
|
+
// Start playback with optional delay
|
|
686
|
+
if (delay > 0) {
|
|
687
|
+
setTimeout(() => this.startPlayback(), delay);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
this.startPlayback();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Pause the animation playback.
|
|
695
|
+
*/
|
|
696
|
+
pause() {
|
|
697
|
+
this.pausePlayback();
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Seek to a specific progress value (0 to 1).
|
|
701
|
+
* Unlike setProgress, this doesn't affect the playing state.
|
|
702
|
+
* If playing, the animation will continue from the new position.
|
|
703
|
+
*/
|
|
704
|
+
seek(progress) {
|
|
705
|
+
var _a, _b;
|
|
706
|
+
this.currentProgress = clamp(progress, 0, 1);
|
|
707
|
+
this.render(this.currentProgress);
|
|
708
|
+
(_b = (_a = this.options).onProgress) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentProgress);
|
|
709
|
+
// If playing, reset the start time to continue from current position
|
|
710
|
+
if (this._isPlaying) {
|
|
711
|
+
const rawProgress = this.playbackDirection === 1
|
|
712
|
+
? this.currentProgress
|
|
713
|
+
: 1 - this.currentProgress;
|
|
714
|
+
// Invert the easing to find approximate time offset
|
|
715
|
+
// For simplicity, use linear approximation
|
|
716
|
+
this.playbackStartTime = performance.now() - (rawProgress * this.playbackDuration);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// ==================== Private Playback Helpers ====================
|
|
720
|
+
startPlayback() {
|
|
721
|
+
this._isPlaying = true;
|
|
722
|
+
this.playbackStartTime = performance.now();
|
|
723
|
+
this.playbackTick();
|
|
724
|
+
}
|
|
725
|
+
pausePlayback() {
|
|
726
|
+
this._isPlaying = false;
|
|
727
|
+
if (this.playbackRafId !== null) {
|
|
728
|
+
cancelAnimationFrame(this.playbackRafId);
|
|
729
|
+
this.playbackRafId = null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
546
732
|
async createCharacterPaths() {
|
|
547
733
|
var _a, _b;
|
|
548
734
|
if (!this.svg)
|
|
@@ -834,6 +1020,7 @@ export class NativeTextPlayer {
|
|
|
834
1020
|
destroy() {
|
|
835
1021
|
var _a, _b;
|
|
836
1022
|
this.stop();
|
|
1023
|
+
this.pausePlayback(); // Clean up playback animation
|
|
837
1024
|
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
838
1025
|
(_b = this.resizeObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
|
|
839
1026
|
if (this.resizeHandler) {
|
package/dist/types.d.ts
CHANGED
|
@@ -46,6 +46,25 @@ export type ScrollAnimationOptions = {
|
|
|
46
46
|
* Options for native text animation (no pre-rendered assets).
|
|
47
47
|
* Replicates Manim's Write/DrawBorderThenFill animation in the browser.
|
|
48
48
|
*/
|
|
49
|
+
/** Easing function type: takes a progress value (0 to 1) and returns eased progress */
|
|
50
|
+
export type EasingFunction = (t: number) => number;
|
|
51
|
+
/** Pre-built easing presets */
|
|
52
|
+
export type EasingPreset = "linear" | "ease-in" | "ease-out" | "ease-in-out" | "smooth";
|
|
53
|
+
/** Options for duration-based animation playback */
|
|
54
|
+
export type PlaybackOptions = {
|
|
55
|
+
/** Animation duration in milliseconds */
|
|
56
|
+
duration?: number;
|
|
57
|
+
/** Delay before starting in milliseconds */
|
|
58
|
+
delay?: number;
|
|
59
|
+
/** Easing function or preset */
|
|
60
|
+
easing?: EasingPreset | EasingFunction;
|
|
61
|
+
/** Whether to loop the animation */
|
|
62
|
+
loop?: boolean;
|
|
63
|
+
/** Play direction: 1 for forward, -1 for reverse */
|
|
64
|
+
direction?: 1 | -1;
|
|
65
|
+
/** Callback when playback completes (not called if loop is true) */
|
|
66
|
+
onComplete?: () => void;
|
|
67
|
+
};
|
|
49
68
|
export type NativeAnimationOptions = {
|
|
50
69
|
/** The container element to render the animation into */
|
|
51
70
|
container: HTMLElement;
|
|
@@ -59,8 +78,13 @@ export type NativeAnimationOptions = {
|
|
|
59
78
|
fontUrl?: string;
|
|
60
79
|
/** Stroke width for the drawing phase (default: 2, matches Manim's DrawBorderThenFill) */
|
|
61
80
|
strokeWidth?: number;
|
|
62
|
-
/** Scroll range configuration */
|
|
81
|
+
/** Scroll range configuration. Ignored when progress is provided. */
|
|
63
82
|
scrollRange?: ScrollRangeValue;
|
|
83
|
+
/**
|
|
84
|
+
* Explicit progress value (0 to 1). When provided, disables scroll-based control
|
|
85
|
+
* and renders the animation at this exact progress.
|
|
86
|
+
*/
|
|
87
|
+
progress?: number;
|
|
64
88
|
/** Called when animation is loaded and ready */
|
|
65
89
|
onReady?: () => void;
|
|
66
90
|
/** Called on scroll progress updates */
|