@mihirsarya/manim-scroll-runtime 0.1.2 → 0.2.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/README.md +231 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/loader.d.ts +3 -0
- package/dist/loader.js +33 -4
- package/dist/native-player.d.ts +65 -0
- package/dist/native-player.js +985 -0
- package/dist/player.d.ts +13 -0
- package/dist/player.js +130 -11
- package/dist/types.d.ts +30 -0
- package/package.json +5 -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,3 +1,5 @@
|
|
|
1
|
+
import { NativeTextPlayer, registerNativeAnimation } from "./native-player";
|
|
1
2
|
import type { ScrollAnimationOptions } from "./types";
|
|
2
|
-
export type { RenderManifest, ScrollAnimationOptions, ScrollRange, ScrollRangePreset, ScrollRangeValue, } from "./types";
|
|
3
|
+
export type { RenderManifest, ScrollAnimationOptions, ScrollRange, ScrollRangePreset, ScrollRangeValue, NativeAnimationOptions, } from "./types";
|
|
4
|
+
export { NativeTextPlayer, registerNativeAnimation };
|
|
3
5
|
export declare function registerScrollAnimation(options: ScrollAnimationOptions): Promise<() => void>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ScrollPlayer } from "./player";
|
|
2
|
+
import { NativeTextPlayer, registerNativeAnimation } from "./native-player";
|
|
3
|
+
export { NativeTextPlayer, registerNativeAnimation };
|
|
2
4
|
export async function registerScrollAnimation(options) {
|
|
3
5
|
const player = new ScrollPlayer(options);
|
|
4
6
|
await player.init();
|
package/dist/loader.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ export declare class FrameCache {
|
|
|
4
4
|
private readonly frameUrls;
|
|
5
5
|
private frames;
|
|
6
6
|
private loading;
|
|
7
|
+
private preloadAhead;
|
|
7
8
|
constructor(frameUrls: string[]);
|
|
8
9
|
get length(): number;
|
|
9
10
|
load(index: number): Promise<HTMLImageElement>;
|
|
11
|
+
private loadFrame;
|
|
12
|
+
private preloadNearby;
|
|
10
13
|
}
|
package/dist/loader.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an asset URL relative to the manifest URL.
|
|
3
|
+
* Handles both absolute manifest URLs (with protocol) and path-only URLs.
|
|
4
|
+
*/
|
|
1
5
|
function resolveAssetUrl(asset, manifestUrl) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
catch {
|
|
6
|
+
// If asset is already absolute, return as-is
|
|
7
|
+
if (asset.startsWith("http://") || asset.startsWith("https://") || asset.startsWith("/")) {
|
|
6
8
|
return asset;
|
|
7
9
|
}
|
|
10
|
+
// Get the directory of the manifest
|
|
11
|
+
const lastSlash = manifestUrl.lastIndexOf("/");
|
|
12
|
+
const baseDir = lastSlash >= 0 ? manifestUrl.substring(0, lastSlash + 1) : "";
|
|
13
|
+
// Resolve relative path
|
|
14
|
+
return baseDir + asset;
|
|
8
15
|
}
|
|
9
16
|
export async function loadManifest(url) {
|
|
10
17
|
const response = await fetch(url);
|
|
@@ -23,17 +30,26 @@ export class FrameCache {
|
|
|
23
30
|
this.frameUrls = frameUrls;
|
|
24
31
|
this.frames = new Map();
|
|
25
32
|
this.loading = new Map();
|
|
33
|
+
this.preloadAhead = 5; // Number of frames to preload ahead
|
|
26
34
|
}
|
|
27
35
|
get length() {
|
|
28
36
|
return this.frameUrls.length;
|
|
29
37
|
}
|
|
30
38
|
async load(index) {
|
|
39
|
+
// Start preloading nearby frames
|
|
40
|
+
this.preloadNearby(index);
|
|
31
41
|
if (this.frames.has(index)) {
|
|
32
42
|
return this.frames.get(index);
|
|
33
43
|
}
|
|
34
44
|
if (this.loading.has(index)) {
|
|
35
45
|
return this.loading.get(index);
|
|
36
46
|
}
|
|
47
|
+
return this.loadFrame(index);
|
|
48
|
+
}
|
|
49
|
+
async loadFrame(index) {
|
|
50
|
+
if (index < 0 || index >= this.frameUrls.length) {
|
|
51
|
+
throw new Error(`Frame index out of bounds: ${index}`);
|
|
52
|
+
}
|
|
37
53
|
const url = this.frameUrls[index];
|
|
38
54
|
const promise = new Promise((resolve, reject) => {
|
|
39
55
|
const img = new Image();
|
|
@@ -51,4 +67,17 @@ export class FrameCache {
|
|
|
51
67
|
this.loading.set(index, promise);
|
|
52
68
|
return promise;
|
|
53
69
|
}
|
|
70
|
+
preloadNearby(currentIndex) {
|
|
71
|
+
// Preload frames ahead and behind
|
|
72
|
+
for (let offset = 1; offset <= this.preloadAhead; offset++) {
|
|
73
|
+
const ahead = currentIndex + offset;
|
|
74
|
+
const behind = currentIndex - offset;
|
|
75
|
+
if (ahead < this.frameUrls.length && !this.frames.has(ahead) && !this.loading.has(ahead)) {
|
|
76
|
+
this.loadFrame(ahead).catch(() => { }); // Silently ignore preload failures
|
|
77
|
+
}
|
|
78
|
+
if (behind >= 0 && !this.frames.has(behind) && !this.loading.has(behind)) {
|
|
79
|
+
this.loadFrame(behind).catch(() => { }); // Silently ignore preload failures
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
54
83
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { NativeAnimationOptions } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* NativeTextPlayer - Renders text animation natively in the browser
|
|
4
|
+
* using SVG paths, replicating Manim's Write/DrawBorderThenFill animation.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1 (progress 0 to 0.5): Draw the stroke progressively
|
|
7
|
+
* Phase 2 (progress 0.5 to 1.0): Fill in the text
|
|
8
|
+
*
|
|
9
|
+
* Key difference from naive implementations: we split each character into
|
|
10
|
+
* individual contours (sub-paths) and apply the lag_ratio to ALL contours
|
|
11
|
+
* across all characters. This matches Manim's behavior where outlines
|
|
12
|
+
* appear progressively rather than all at once.
|
|
13
|
+
*/
|
|
14
|
+
export declare class NativeTextPlayer {
|
|
15
|
+
private readonly container;
|
|
16
|
+
private readonly options;
|
|
17
|
+
private svg;
|
|
18
|
+
private fallbackWrapper;
|
|
19
|
+
/** All sub-paths (segments) across all characters, for stroke animation */
|
|
20
|
+
private subPaths;
|
|
21
|
+
/** Fill paths (original closed contours) for the filled state */
|
|
22
|
+
private fillPaths;
|
|
23
|
+
private isActive;
|
|
24
|
+
private rafId;
|
|
25
|
+
private observer?;
|
|
26
|
+
private resizeObserver?;
|
|
27
|
+
private lastProgress;
|
|
28
|
+
private scrollHandler?;
|
|
29
|
+
private resizeHandler?;
|
|
30
|
+
private pendingDraw;
|
|
31
|
+
private pendingResize;
|
|
32
|
+
private font;
|
|
33
|
+
/** Last known font size, used to detect changes for inherited sizing */
|
|
34
|
+
private lastComputedFontSize;
|
|
35
|
+
constructor(options: NativeAnimationOptions);
|
|
36
|
+
init(): Promise<void>;
|
|
37
|
+
private createCharacterPaths;
|
|
38
|
+
/**
|
|
39
|
+
* Get the inherited font size from the container's computed style.
|
|
40
|
+
*/
|
|
41
|
+
private getInheritedFontSize;
|
|
42
|
+
private createFallbackTextAnimation;
|
|
43
|
+
private render;
|
|
44
|
+
destroy(): void;
|
|
45
|
+
private setupObserver;
|
|
46
|
+
/**
|
|
47
|
+
* Set up resize handling for responsive behavior:
|
|
48
|
+
* 1. Window resize - recalculate scroll progress (viewport height changes)
|
|
49
|
+
* 2. Container resize - detect font-size changes when using inherited sizing
|
|
50
|
+
*/
|
|
51
|
+
private setupResizeHandling;
|
|
52
|
+
/**
|
|
53
|
+
* Rebuild the animation when font size changes (for inherited sizing).
|
|
54
|
+
* This clears and recreates all character paths with the new size.
|
|
55
|
+
*/
|
|
56
|
+
private rebuildAnimation;
|
|
57
|
+
private start;
|
|
58
|
+
private stop;
|
|
59
|
+
private tick;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Register a native text animation on a container element.
|
|
63
|
+
* This creates scroll-driven text animation without pre-rendered assets.
|
|
64
|
+
*/
|
|
65
|
+
export declare function registerNativeAnimation(options: NativeAnimationOptions): Promise<() => void>;
|