@mihirsarya/manim-scroll-react 0.2.1 → 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.
@@ -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;
@@ -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 scroll progress (0 to 1) */
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
- * function NativeTextAnimation() {
135
- * const containerRef = useRef<HTMLDivElement>(null);
136
- * const { progress, isReady } = useNativeAnimation({
137
- * ref: containerRef,
138
- * text: "Hello World",
139
- * fontSize: 48,
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
- * return (
144
- * <div ref={containerRef} style={{ height: "100vh" }}>
145
- * {!isReady && <div>Loading...</div>}
146
- * </div>
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, registerNativeAnimation } from "@mihirsarya/manim-scroll-runtime";
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
- * function NativeTextAnimation() {
240
- * const containerRef = useRef<HTMLDivElement>(null);
241
- * const { progress, isReady } = useNativeAnimation({
242
- * ref: containerRef,
243
- * text: "Hello World",
244
- * fontSize: 48,
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
- * return (
249
- * <div ref={containerRef} style={{ height: "100vh" }}>
250
- * {!isReady && <div>Loading...</div>}
251
- * </div>
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, setProgress] = useState(0);
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 cleanupRef = useRef(null);
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
- setProgress(p);
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 nativeOptions = {
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
- registerNativeAnimation(nativeOptions)
300
- .then((dispose) => {
311
+ });
312
+ player.init()
313
+ .then(() => {
301
314
  if (!isMounted) {
302
- dispose();
315
+ player.destroy();
303
316
  return;
304
317
  }
305
- cleanupRef.current = dispose;
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
- (_a = cleanupRef.current) === null || _a === void 0 ? void 0 : _a.call(cleanupRef);
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.1",
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.1"
15
+ "@mihirsarya/manim-scroll-runtime": "0.2.2"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/react": "^18.2.61",