@mihirsarya/manim-scroll-react 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 +230 -0
- package/dist/ManimScroll.d.ts +8 -2
- package/dist/ManimScroll.js +2 -1
- package/dist/hooks.d.ts +62 -18
- package/dist/hooks.js +88 -29
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# @mihirsarya/manim-scroll-react
|
|
2
|
+
|
|
3
|
+
React components and hooks for scroll-driven Manim animations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mihirsarya/manim-scroll-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use the unified package (recommended):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @mihirsarya/manim-scroll
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- React 18+
|
|
20
|
+
- `@mihirsarya/manim-scroll-runtime` (peer dependency, installed automatically)
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### With Next.js Plugin (Recommended)
|
|
25
|
+
|
|
26
|
+
When using with `@mihirsarya/manim-scroll-next`, animations are automatically resolved:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { ManimScroll } from "@mihirsarya/manim-scroll-react";
|
|
30
|
+
|
|
31
|
+
export default function Page() {
|
|
32
|
+
return (
|
|
33
|
+
<ManimScroll
|
|
34
|
+
scene="TextScene"
|
|
35
|
+
fontSize={72}
|
|
36
|
+
color="#ffffff"
|
|
37
|
+
scrollRange="viewport"
|
|
38
|
+
>
|
|
39
|
+
Welcome to my site
|
|
40
|
+
</ManimScroll>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Manual Mode
|
|
46
|
+
|
|
47
|
+
Without the Next.js plugin, provide an explicit manifest URL:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { ManimScroll } from "@mihirsarya/manim-scroll-react";
|
|
51
|
+
|
|
52
|
+
export default function Page() {
|
|
53
|
+
return (
|
|
54
|
+
<ManimScroll
|
|
55
|
+
manifestUrl="/assets/scene/manifest.json"
|
|
56
|
+
scrollRange="viewport"
|
|
57
|
+
>
|
|
58
|
+
Scroll-driven text
|
|
59
|
+
</ManimScroll>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Native Mode
|
|
65
|
+
|
|
66
|
+
For text animations without pre-rendered assets:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<ManimScroll mode="native" fontSize={48} color="#ffffff">
|
|
70
|
+
Animate this text
|
|
71
|
+
</ManimScroll>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Exports
|
|
75
|
+
|
|
76
|
+
### Components
|
|
77
|
+
|
|
78
|
+
- **`ManimScroll`** - Scroll-driven animation component
|
|
79
|
+
|
|
80
|
+
### Hooks
|
|
81
|
+
|
|
82
|
+
- **`useManimScroll`** - Hook for custom integrations with pre-rendered assets
|
|
83
|
+
- **`useNativeAnimation`** - Hook for native SVG text animation
|
|
84
|
+
|
|
85
|
+
### Utilities
|
|
86
|
+
|
|
87
|
+
- **`computePropsHash`** - Compute hash for animation props
|
|
88
|
+
- **`extractChildrenText`** - Extract text from React children
|
|
89
|
+
|
|
90
|
+
### Types
|
|
91
|
+
|
|
92
|
+
- `ManimScrollProps`, `ManimAnimationProps`
|
|
93
|
+
- `UseManimScrollOptions`, `UseManimScrollResult`
|
|
94
|
+
- `UseNativeAnimationOptions`, `UseNativeAnimationResult`
|
|
95
|
+
|
|
96
|
+
## ManimScroll Props
|
|
97
|
+
|
|
98
|
+
| Prop | Type | Default | Description |
|
|
99
|
+
|------|------|---------|-------------|
|
|
100
|
+
| `scene` | `string` | `"TextScene"` | Manim scene name |
|
|
101
|
+
| `fontSize` | `number` | - | Font size for text animations |
|
|
102
|
+
| `color` | `string` | - | Color as hex string |
|
|
103
|
+
| `font` | `string` | - | Font family |
|
|
104
|
+
| `inline` | `boolean` | `false` | Enable inline mode |
|
|
105
|
+
| `padding` | `number` | `0.2` | Padding in inline mode (Manim units) |
|
|
106
|
+
| `mode` | `"auto" \| "video" \| "frames" \| "native"` | `"auto"` | Playback mode |
|
|
107
|
+
| `scrollRange` | `ScrollRangeValue` | `"viewport"` | Scroll range configuration |
|
|
108
|
+
| `manifestUrl` | `string` | - | Explicit manifest URL |
|
|
109
|
+
| `fontUrl` | `string` | - | Font file URL for native mode |
|
|
110
|
+
| `strokeWidth` | `number` | `2` | Stroke width for native mode |
|
|
111
|
+
| `onReady` | `() => void` | - | Called when animation is loaded |
|
|
112
|
+
| `onProgress` | `(progress: number) => void` | - | Called on scroll progress |
|
|
113
|
+
| `canvas` | `{ width?, height? }` | - | Canvas dimensions |
|
|
114
|
+
| `className` | `string` | - | CSS class |
|
|
115
|
+
| `style` | `CSSProperties` | - | Inline styles |
|
|
116
|
+
| `children` | `ReactNode` | - | Text content |
|
|
117
|
+
|
|
118
|
+
## useManimScroll Hook
|
|
119
|
+
|
|
120
|
+
For advanced use cases requiring custom control:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { useRef } from "react";
|
|
124
|
+
import { useManimScroll } from "@mihirsarya/manim-scroll-react";
|
|
125
|
+
|
|
126
|
+
function CustomAnimation() {
|
|
127
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
128
|
+
|
|
129
|
+
const { progress, isReady, error, pause, resume, seek } = useManimScroll({
|
|
130
|
+
ref: containerRef,
|
|
131
|
+
manifestUrl: "/assets/scene/manifest.json",
|
|
132
|
+
scrollRange: "viewport",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div ref={containerRef} style={{ height: "100vh" }}>
|
|
137
|
+
{!isReady && <div>Loading...</div>}
|
|
138
|
+
{error && <div>Error: {error.message}</div>}
|
|
139
|
+
<div>Progress: {Math.round(progress * 100)}%</div>
|
|
140
|
+
<button onClick={pause}>Pause</button>
|
|
141
|
+
<button onClick={resume}>Resume</button>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Hook Options
|
|
148
|
+
|
|
149
|
+
| Option | Type | Description |
|
|
150
|
+
|--------|------|-------------|
|
|
151
|
+
| `ref` | `RefObject<HTMLElement>` | Container element ref (required) |
|
|
152
|
+
| `manifestUrl` | `string` | Explicit manifest URL |
|
|
153
|
+
| `scene` | `string` | Scene name for auto-resolution |
|
|
154
|
+
| `animationProps` | `Record<string, unknown>` | Props for auto-resolution |
|
|
155
|
+
| `mode` | `"auto" \| "frames" \| "video"` | Playback mode |
|
|
156
|
+
| `scrollRange` | `ScrollRangeValue` | Scroll range configuration |
|
|
157
|
+
| `canvasDimensions` | `{ width?, height? }` | Canvas size |
|
|
158
|
+
| `enabled` | `boolean` | Whether the hook is active |
|
|
159
|
+
|
|
160
|
+
### Hook Return Value
|
|
161
|
+
|
|
162
|
+
| Property | Type | Description |
|
|
163
|
+
|----------|------|-------------|
|
|
164
|
+
| `progress` | `number` | Current scroll progress (0 to 1) |
|
|
165
|
+
| `isReady` | `boolean` | Whether animation is loaded |
|
|
166
|
+
| `error` | `Error \| null` | Loading error, if any |
|
|
167
|
+
| `canvasRef` | `RefObject<HTMLCanvasElement>` | Canvas element ref |
|
|
168
|
+
| `seek` | `(progress: number) => void` | Seek to specific progress |
|
|
169
|
+
| `pause` | `() => void` | Pause scroll-driven updates |
|
|
170
|
+
| `resume` | `() => void` | Resume scroll-driven updates |
|
|
171
|
+
| `isPaused` | `boolean` | Whether updates are paused |
|
|
172
|
+
|
|
173
|
+
## useNativeAnimation Hook
|
|
174
|
+
|
|
175
|
+
For native SVG text animation without pre-rendered assets:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { useRef } from "react";
|
|
179
|
+
import { useNativeAnimation } from "@mihirsarya/manim-scroll-react";
|
|
180
|
+
|
|
181
|
+
function NativeTextAnimation() {
|
|
182
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
183
|
+
|
|
184
|
+
const { progress, isReady } = useNativeAnimation({
|
|
185
|
+
ref: containerRef,
|
|
186
|
+
text: "Hello World",
|
|
187
|
+
fontSize: 48,
|
|
188
|
+
color: "#ffffff",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div ref={containerRef} style={{ height: "100vh" }}>
|
|
193
|
+
{!isReady && <div>Loading...</div>}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Hook Options
|
|
200
|
+
|
|
201
|
+
| Option | Type | Description |
|
|
202
|
+
|--------|------|-------------|
|
|
203
|
+
| `ref` | `RefObject<HTMLElement>` | Container element ref (required) |
|
|
204
|
+
| `text` | `string` | Text to animate (required) |
|
|
205
|
+
| `fontSize` | `number` | Font size in pixels (inherits from parent if not set) |
|
|
206
|
+
| `color` | `string` | Text color |
|
|
207
|
+
| `fontUrl` | `string` | URL to font file for opentype.js |
|
|
208
|
+
| `strokeWidth` | `number` | Stroke width for drawing phase |
|
|
209
|
+
| `scrollRange` | `ScrollRangeValue` | Scroll range configuration |
|
|
210
|
+
| `enabled` | `boolean` | Whether the hook is active |
|
|
211
|
+
|
|
212
|
+
## Scroll Range Configuration
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Presets
|
|
216
|
+
scrollRange="viewport" // Animation plays as element crosses viewport
|
|
217
|
+
scrollRange="element" // Tied to element's own scroll position
|
|
218
|
+
scrollRange="full" // Spans entire document scroll
|
|
219
|
+
|
|
220
|
+
// Relative units
|
|
221
|
+
scrollRange={["100vh", "-50%"]}
|
|
222
|
+
|
|
223
|
+
// Pixel values
|
|
224
|
+
scrollRange={[800, -400]}
|
|
225
|
+
scrollRange={{ start: 800, end: -400 }}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
package/dist/ManimScroll.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type ManimScrollProps = ManimAnimationProps & {
|
|
|
34
34
|
* - "native": Uses native SVG animation (no pre-rendered assets)
|
|
35
35
|
*/
|
|
36
36
|
mode?: "auto" | "frames" | "video" | "native";
|
|
37
|
-
/** Scroll range configuration (preset, tuple, or legacy object) */
|
|
37
|
+
/** Scroll range configuration (preset, tuple, or legacy object). Ignored when progress is provided. */
|
|
38
38
|
scrollRange?: ScrollRangeValue;
|
|
39
39
|
/** Called when animation is loaded and ready */
|
|
40
40
|
onReady?: () => void;
|
|
@@ -49,6 +49,12 @@ export type ManimScrollProps = ManimAnimationProps & {
|
|
|
49
49
|
fontUrl?: string;
|
|
50
50
|
/** Stroke width for native mode drawing phase */
|
|
51
51
|
strokeWidth?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Explicit progress value (0 to 1). When provided, disables scroll-based control
|
|
54
|
+
* and renders the animation at this exact progress (controlled mode).
|
|
55
|
+
* Works with native mode. For advanced control, use useNativeAnimation hook.
|
|
56
|
+
*/
|
|
57
|
+
progress?: number;
|
|
52
58
|
className?: string;
|
|
53
59
|
style?: React.CSSProperties;
|
|
54
60
|
children?: React.ReactNode;
|
|
@@ -78,4 +84,4 @@ export type ManimScrollProps = ManimAnimationProps & {
|
|
|
78
84
|
* </ManimScroll>
|
|
79
85
|
* ```
|
|
80
86
|
*/
|
|
81
|
-
export declare function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, fontUrl, strokeWidth, scene, fontSize, color, font, inline, padding, ...customProps }: ManimScrollProps): JSX.Element;
|
|
87
|
+
export declare function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, fontUrl, strokeWidth, progress: controlledProgress, scene, fontSize, color, font, inline, padding, ...customProps }: ManimScrollProps): JSX.Element;
|
package/dist/ManimScroll.js
CHANGED
|
@@ -27,7 +27,7 @@ import { extractChildrenText } from "./hash";
|
|
|
27
27
|
* </ManimScroll>
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
export function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, fontUrl, strokeWidth,
|
|
30
|
+
export function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, fontUrl, strokeWidth, progress: controlledProgress,
|
|
31
31
|
// Animation props
|
|
32
32
|
scene = "TextScene", fontSize, color, font, inline, padding, ...customProps }) {
|
|
33
33
|
const containerRef = useRef(null);
|
|
@@ -72,6 +72,7 @@ scene = "TextScene", fontSize, color, font, inline, padding, ...customProps }) {
|
|
|
72
72
|
fontUrl,
|
|
73
73
|
strokeWidth,
|
|
74
74
|
scrollRange,
|
|
75
|
+
progress: controlledProgress, // Pass progress for controlled mode
|
|
75
76
|
enabled: isNativeMode,
|
|
76
77
|
});
|
|
77
78
|
// Select the appropriate result based on mode
|
package/dist/hooks.d.ts
CHANGED
|
@@ -100,27 +100,60 @@ export interface UseNativeAnimationOptions {
|
|
|
100
100
|
fontUrl?: string;
|
|
101
101
|
/** Stroke width for the drawing phase */
|
|
102
102
|
strokeWidth?: number;
|
|
103
|
-
/** Scroll range configuration */
|
|
103
|
+
/** Scroll range configuration. Ignored when progress is provided. */
|
|
104
104
|
scrollRange?: ScrollRangeValue;
|
|
105
|
+
/**
|
|
106
|
+
* Explicit progress value (0 to 1). When provided, disables scroll-based control
|
|
107
|
+
* and renders the animation at this exact progress (controlled mode).
|
|
108
|
+
*/
|
|
109
|
+
progress?: number;
|
|
105
110
|
/** Whether the hook is enabled (default: true) */
|
|
106
111
|
enabled?: boolean;
|
|
107
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Playback options for the play() method.
|
|
115
|
+
*/
|
|
116
|
+
export interface NativePlaybackOptions {
|
|
117
|
+
/** Animation duration in milliseconds */
|
|
118
|
+
duration?: number;
|
|
119
|
+
/** Delay before starting in milliseconds */
|
|
120
|
+
delay?: number;
|
|
121
|
+
/** Easing preset or custom function */
|
|
122
|
+
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out" | "smooth" | ((t: number) => number);
|
|
123
|
+
/** Whether to loop the animation */
|
|
124
|
+
loop?: boolean;
|
|
125
|
+
/** Play direction: 1 for forward, -1 for reverse */
|
|
126
|
+
direction?: 1 | -1;
|
|
127
|
+
/** Callback when playback completes */
|
|
128
|
+
onComplete?: () => void;
|
|
129
|
+
}
|
|
108
130
|
/**
|
|
109
131
|
* Result returned by the useNativeAnimation hook.
|
|
110
132
|
*/
|
|
111
133
|
export interface UseNativeAnimationResult {
|
|
112
|
-
/** Current
|
|
134
|
+
/** Current animation progress (0 to 1) */
|
|
113
135
|
progress: number;
|
|
114
136
|
/** Whether the animation is loaded and ready */
|
|
115
137
|
isReady: boolean;
|
|
116
138
|
/** Error if initialization failed */
|
|
117
139
|
error: Error | null;
|
|
118
|
-
/** Pause scroll-driven updates */
|
|
140
|
+
/** Pause scroll-driven updates (legacy, use pause() for playback) */
|
|
119
141
|
pause: () => void;
|
|
120
|
-
/** Resume scroll-driven updates */
|
|
142
|
+
/** Resume scroll-driven updates (legacy) */
|
|
121
143
|
resume: () => void;
|
|
122
144
|
/** Whether scroll updates are paused */
|
|
123
145
|
isPaused: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Play animation over a duration.
|
|
148
|
+
* @param options - Duration in ms, or PlaybackOptions object
|
|
149
|
+
*/
|
|
150
|
+
play: (options?: NativePlaybackOptions | number) => void;
|
|
151
|
+
/** Seek to specific progress (0-1). Doesn't affect playing state. */
|
|
152
|
+
seek: (progress: number) => void;
|
|
153
|
+
/** Set progress and stop any playback. For controlled mode. */
|
|
154
|
+
setProgress: (progress: number) => void;
|
|
155
|
+
/** Whether time-based animation is currently playing */
|
|
156
|
+
isPlaying: boolean;
|
|
124
157
|
}
|
|
125
158
|
/**
|
|
126
159
|
* Hook for native text animation that renders directly in the browser
|
|
@@ -129,23 +162,34 @@ export interface UseNativeAnimationResult {
|
|
|
129
162
|
* This hook bypasses the manifest resolution and pre-rendered assets,
|
|
130
163
|
* instead animating text natively using opentype.js and SVG.
|
|
131
164
|
*
|
|
165
|
+
* Supports three usage modes:
|
|
166
|
+
* 1. Scroll-driven (default): Animation driven by scroll position
|
|
167
|
+
* 2. Controlled: Pass progress prop for React-style controlled components
|
|
168
|
+
* 3. Imperative: Use play(), seek(), setProgress() for programmatic control
|
|
169
|
+
*
|
|
132
170
|
* @example
|
|
133
171
|
* ```tsx
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* color: "#ffffff",
|
|
141
|
-
* });
|
|
172
|
+
* // Scroll-driven mode (default)
|
|
173
|
+
* const { progress, isReady } = useNativeAnimation({
|
|
174
|
+
* ref: containerRef,
|
|
175
|
+
* text: "Hello World",
|
|
176
|
+
* fontSize: 48,
|
|
177
|
+
* });
|
|
142
178
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
179
|
+
* // Controlled mode
|
|
180
|
+
* const [progress, setProgress] = useState(0);
|
|
181
|
+
* useNativeAnimation({
|
|
182
|
+
* ref: containerRef,
|
|
183
|
+
* text: "Hello World",
|
|
184
|
+
* progress, // Animation renders at this exact progress
|
|
185
|
+
* });
|
|
186
|
+
*
|
|
187
|
+
* // Imperative mode
|
|
188
|
+
* const { play, isReady } = useNativeAnimation({
|
|
189
|
+
* ref: containerRef,
|
|
190
|
+
* text: "Hello World",
|
|
191
|
+
* });
|
|
192
|
+
* useEffect(() => { if (isReady) play(2000); }, [isReady]); // Play over 2s
|
|
149
193
|
* ```
|
|
150
194
|
*/
|
|
151
195
|
export declare function useNativeAnimation(options: UseNativeAnimationOptions): UseNativeAnimationResult;
|
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
|
2
|
-
import { registerScrollAnimation,
|
|
2
|
+
import { registerScrollAnimation, NativeTextPlayer } from "@mihirsarya/manim-scroll-runtime";
|
|
3
3
|
import { computePropsHash } from "./hash";
|
|
4
4
|
// Global cache manifest state
|
|
5
5
|
let cachedManifest = null;
|
|
@@ -234,43 +234,55 @@ export { loadCacheManifest, resolveManifestUrl };
|
|
|
234
234
|
* This hook bypasses the manifest resolution and pre-rendered assets,
|
|
235
235
|
* instead animating text natively using opentype.js and SVG.
|
|
236
236
|
*
|
|
237
|
+
* Supports three usage modes:
|
|
238
|
+
* 1. Scroll-driven (default): Animation driven by scroll position
|
|
239
|
+
* 2. Controlled: Pass progress prop for React-style controlled components
|
|
240
|
+
* 3. Imperative: Use play(), seek(), setProgress() for programmatic control
|
|
241
|
+
*
|
|
237
242
|
* @example
|
|
238
243
|
* ```tsx
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* color: "#ffffff",
|
|
246
|
-
* });
|
|
244
|
+
* // Scroll-driven mode (default)
|
|
245
|
+
* const { progress, isReady } = useNativeAnimation({
|
|
246
|
+
* ref: containerRef,
|
|
247
|
+
* text: "Hello World",
|
|
248
|
+
* fontSize: 48,
|
|
249
|
+
* });
|
|
247
250
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
251
|
+
* // Controlled mode
|
|
252
|
+
* const [progress, setProgress] = useState(0);
|
|
253
|
+
* useNativeAnimation({
|
|
254
|
+
* ref: containerRef,
|
|
255
|
+
* text: "Hello World",
|
|
256
|
+
* progress, // Animation renders at this exact progress
|
|
257
|
+
* });
|
|
258
|
+
*
|
|
259
|
+
* // Imperative mode
|
|
260
|
+
* const { play, isReady } = useNativeAnimation({
|
|
261
|
+
* ref: containerRef,
|
|
262
|
+
* text: "Hello World",
|
|
263
|
+
* });
|
|
264
|
+
* useEffect(() => { if (isReady) play(2000); }, [isReady]); // Play over 2s
|
|
254
265
|
* ```
|
|
255
266
|
*/
|
|
256
267
|
export function useNativeAnimation(options) {
|
|
257
268
|
const { ref, text, fontSize, // undefined means inherit from parent
|
|
258
269
|
color = "#ffffff", fontUrl, strokeWidth = 2, // Manim's DrawBorderThenFill default
|
|
259
|
-
scrollRange, enabled = true, } = options;
|
|
260
|
-
const [progress,
|
|
270
|
+
scrollRange, progress: controlledProgress, enabled = true, } = options;
|
|
271
|
+
const [progress, setProgressState] = useState(controlledProgress !== null && controlledProgress !== void 0 ? controlledProgress : 0);
|
|
261
272
|
const [isReady, setIsReady] = useState(false);
|
|
262
273
|
const [error, setError] = useState(null);
|
|
263
274
|
const [isPaused, setIsPaused] = useState(false);
|
|
264
|
-
const
|
|
275
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
276
|
+
const playerRef = useRef(null);
|
|
265
277
|
const pausedRef = useRef(isPaused);
|
|
266
278
|
// Keep pausedRef in sync
|
|
267
279
|
useEffect(() => {
|
|
268
280
|
pausedRef.current = isPaused;
|
|
269
281
|
}, [isPaused]);
|
|
270
|
-
// Handle progress updates (respects pause state)
|
|
282
|
+
// Handle progress updates (respects pause state for scroll-driven mode)
|
|
271
283
|
const handleProgress = useCallback((p) => {
|
|
272
284
|
if (!pausedRef.current) {
|
|
273
|
-
|
|
285
|
+
setProgressState(p);
|
|
274
286
|
}
|
|
275
287
|
}, []);
|
|
276
288
|
// Handle ready callback
|
|
@@ -285,7 +297,7 @@ export function useNativeAnimation(options) {
|
|
|
285
297
|
if (!container || !text)
|
|
286
298
|
return;
|
|
287
299
|
let isMounted = true;
|
|
288
|
-
const
|
|
300
|
+
const player = new NativeTextPlayer({
|
|
289
301
|
container,
|
|
290
302
|
text,
|
|
291
303
|
fontSize,
|
|
@@ -293,16 +305,17 @@ export function useNativeAnimation(options) {
|
|
|
293
305
|
fontUrl,
|
|
294
306
|
strokeWidth,
|
|
295
307
|
scrollRange,
|
|
308
|
+
progress: controlledProgress,
|
|
296
309
|
onReady: handleReady,
|
|
297
310
|
onProgress: handleProgress,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
.then((
|
|
311
|
+
});
|
|
312
|
+
player.init()
|
|
313
|
+
.then(() => {
|
|
301
314
|
if (!isMounted) {
|
|
302
|
-
|
|
315
|
+
player.destroy();
|
|
303
316
|
return;
|
|
304
317
|
}
|
|
305
|
-
|
|
318
|
+
playerRef.current = player;
|
|
306
319
|
})
|
|
307
320
|
.catch((err) => {
|
|
308
321
|
if (isMounted) {
|
|
@@ -310,18 +323,60 @@ export function useNativeAnimation(options) {
|
|
|
310
323
|
}
|
|
311
324
|
});
|
|
312
325
|
return () => {
|
|
313
|
-
var _a;
|
|
314
326
|
isMounted = false;
|
|
315
|
-
|
|
327
|
+
player.destroy();
|
|
328
|
+
playerRef.current = null;
|
|
316
329
|
setIsReady(false);
|
|
317
330
|
};
|
|
318
|
-
}, [enabled, ref, text, fontSize, color, fontUrl, strokeWidth, scrollRange, handleProgress, handleReady]);
|
|
331
|
+
}, [enabled, ref, text, fontSize, color, fontUrl, strokeWidth, scrollRange, controlledProgress, handleProgress, handleReady]);
|
|
332
|
+
// Update progress when controlled prop changes
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (controlledProgress !== undefined && playerRef.current) {
|
|
335
|
+
playerRef.current.setProgress(controlledProgress);
|
|
336
|
+
setProgressState(controlledProgress);
|
|
337
|
+
}
|
|
338
|
+
}, [controlledProgress]);
|
|
339
|
+
// Legacy pause/resume for scroll-driven mode
|
|
319
340
|
const pause = useCallback(() => {
|
|
320
341
|
setIsPaused(true);
|
|
321
342
|
}, []);
|
|
322
343
|
const resume = useCallback(() => {
|
|
323
344
|
setIsPaused(false);
|
|
324
345
|
}, []);
|
|
346
|
+
// Playback controls
|
|
347
|
+
const play = useCallback((playOptions) => {
|
|
348
|
+
const player = playerRef.current;
|
|
349
|
+
if (!player)
|
|
350
|
+
return;
|
|
351
|
+
setIsPlaying(true);
|
|
352
|
+
if (typeof playOptions === "number") {
|
|
353
|
+
player.play({
|
|
354
|
+
duration: playOptions,
|
|
355
|
+
onComplete: () => setIsPlaying(false),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
player.play({
|
|
360
|
+
...playOptions,
|
|
361
|
+
onComplete: () => {
|
|
362
|
+
var _a;
|
|
363
|
+
(_a = playOptions === null || playOptions === void 0 ? void 0 : playOptions.onComplete) === null || _a === void 0 ? void 0 : _a.call(playOptions);
|
|
364
|
+
setIsPlaying(false);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}, []);
|
|
369
|
+
const seek = useCallback((targetProgress) => {
|
|
370
|
+
var _a;
|
|
371
|
+
(_a = playerRef.current) === null || _a === void 0 ? void 0 : _a.seek(targetProgress);
|
|
372
|
+
setProgressState(targetProgress);
|
|
373
|
+
}, []);
|
|
374
|
+
const setProgress = useCallback((targetProgress) => {
|
|
375
|
+
var _a;
|
|
376
|
+
(_a = playerRef.current) === null || _a === void 0 ? void 0 : _a.setProgress(targetProgress);
|
|
377
|
+
setProgressState(targetProgress);
|
|
378
|
+
setIsPlaying(false);
|
|
379
|
+
}, []);
|
|
325
380
|
return {
|
|
326
381
|
progress,
|
|
327
382
|
isReady,
|
|
@@ -329,5 +384,9 @@ export function useNativeAnimation(options) {
|
|
|
329
384
|
pause,
|
|
330
385
|
resume,
|
|
331
386
|
isPaused,
|
|
387
|
+
play,
|
|
388
|
+
seek,
|
|
389
|
+
setProgress,
|
|
390
|
+
isPlaying,
|
|
332
391
|
};
|
|
333
392
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mihirsarya/manim-scroll-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "React wrapper for scroll-driven Manim animations.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"react-dom": ">=18.0.0"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@mihirsarya/manim-scroll-runtime": "0.2.
|
|
15
|
+
"@mihirsarya/manim-scroll-runtime": "0.2.2"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/react": "^18.2.61",
|