@mihirsarya/manim-scroll-react 0.1.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.
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import type { ScrollRangeValue } from "@mihirsarya/manim-scroll-runtime";
3
+ /**
4
+ * Animation props that are passed to the Manim scene.
5
+ * These are used for auto-resolution when manifestUrl is not provided.
6
+ */
7
+ export interface ManimAnimationProps {
8
+ /** Scene name (default: "TextScene") */
9
+ scene?: string;
10
+ /** Font size for text animations */
11
+ fontSize?: number;
12
+ /** Color for the animation (hex string) */
13
+ color?: string;
14
+ /** Font family for text animations */
15
+ font?: string;
16
+ /** Additional custom props for the scene */
17
+ [key: string]: unknown;
18
+ }
19
+ export type ManimScrollProps = ManimAnimationProps & {
20
+ /** Explicit manifest URL (overrides auto-resolution) */
21
+ manifestUrl?: string;
22
+ /** Playback mode */
23
+ mode?: "auto" | "frames" | "video";
24
+ /** Scroll range configuration (preset, tuple, or legacy object) */
25
+ scrollRange?: ScrollRangeValue;
26
+ /** Called when animation is loaded and ready */
27
+ onReady?: () => void;
28
+ /** Called on scroll progress updates */
29
+ onProgress?: (progress: number) => void;
30
+ /** Canvas dimensions */
31
+ canvas?: {
32
+ width?: number;
33
+ height?: number;
34
+ };
35
+ className?: string;
36
+ style?: React.CSSProperties;
37
+ children?: React.ReactNode;
38
+ };
39
+ /**
40
+ * Scroll-driven Manim animation component.
41
+ *
42
+ * Automatically resolves pre-rendered animation assets based on props,
43
+ * or use an explicit `manifestUrl` for manual control.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * // Auto-resolution (with Next.js plugin)
48
+ * <ManimScroll scene="TextScene" fontSize={72} color="#ffffff">
49
+ * Welcome to my site
50
+ * </ManimScroll>
51
+ *
52
+ * // Manual mode
53
+ * <ManimScroll manifestUrl="/assets/scene/manifest.json">
54
+ * Scroll-driven text
55
+ * </ManimScroll>
56
+ * ```
57
+ */
58
+ export declare function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, scene, fontSize, color, font, ...customProps }: ManimScrollProps): JSX.Element;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useMemo, useEffect } from "react";
3
+ import { useManimScroll } from "./hooks";
4
+ import { extractChildrenText } from "./hash";
5
+ /**
6
+ * Scroll-driven Manim animation component.
7
+ *
8
+ * Automatically resolves pre-rendered animation assets based on props,
9
+ * or use an explicit `manifestUrl` for manual control.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * // Auto-resolution (with Next.js plugin)
14
+ * <ManimScroll scene="TextScene" fontSize={72} color="#ffffff">
15
+ * Welcome to my site
16
+ * </ManimScroll>
17
+ *
18
+ * // Manual mode
19
+ * <ManimScroll manifestUrl="/assets/scene/manifest.json">
20
+ * Scroll-driven text
21
+ * </ManimScroll>
22
+ * ```
23
+ */
24
+ export function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress,
25
+ // Animation props
26
+ scene = "TextScene", fontSize, color, font, ...customProps }) {
27
+ const containerRef = useRef(null);
28
+ // Extract text from children
29
+ const childrenText = useMemo(() => extractChildrenText(children), [children]);
30
+ // Build animation props for hashing
31
+ const animationProps = useMemo(() => {
32
+ const props = { ...customProps };
33
+ if (childrenText)
34
+ props.text = childrenText;
35
+ if (fontSize !== undefined)
36
+ props.fontSize = fontSize;
37
+ if (color !== undefined)
38
+ props.color = color;
39
+ if (font !== undefined)
40
+ props.font = font;
41
+ return props;
42
+ }, [childrenText, fontSize, color, font, customProps]);
43
+ // Use the hook for all animation logic
44
+ const { isReady, error, progress } = useManimScroll({
45
+ ref: containerRef,
46
+ manifestUrl,
47
+ scene,
48
+ animationProps,
49
+ mode,
50
+ scrollRange,
51
+ canvasDimensions: canvas,
52
+ });
53
+ // Forward callbacks
54
+ useEffect(() => {
55
+ if (isReady) {
56
+ onReady === null || onReady === void 0 ? void 0 : onReady();
57
+ }
58
+ }, [isReady, onReady]);
59
+ useEffect(() => {
60
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress);
61
+ }, [progress, onProgress]);
62
+ return (_jsxs("div", { className: className, style: { position: "relative", ...style }, ref: containerRef, children: [_jsx("span", { style: { position: "absolute", opacity: 0 }, children: children }), error && process.env.NODE_ENV === "development" && (_jsx("div", { style: {
63
+ position: "absolute",
64
+ inset: 0,
65
+ display: "flex",
66
+ alignItems: "center",
67
+ justifyContent: "center",
68
+ background: "rgba(0, 0, 0, 0.8)",
69
+ color: "#ff6b6b",
70
+ padding: "1rem",
71
+ fontSize: "0.875rem",
72
+ textAlign: "center",
73
+ }, children: error.message }))] }));
74
+ }
package/dist/hash.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Compute a deterministic hash for animation props.
3
+ * This hash is used to look up pre-rendered assets at runtime.
4
+ *
5
+ * Note: This must produce the same hash as the build-time hasher.
6
+ * Uses the djb2 algorithm for cross-environment compatibility.
7
+ */
8
+ export declare function computePropsHash(scene: string, props: Record<string, unknown>): string;
9
+ /**
10
+ * Extract text content from React children.
11
+ * Handles strings and arrays of strings.
12
+ */
13
+ export declare function extractChildrenText(children: React.ReactNode): string | null;
package/dist/hash.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Recursively sort object keys for deterministic JSON stringification.
3
+ */
4
+ function sortObjectKeys(obj) {
5
+ if (obj === null || typeof obj !== "object") {
6
+ return obj;
7
+ }
8
+ if (Array.isArray(obj)) {
9
+ return obj.map(sortObjectKeys);
10
+ }
11
+ const sorted = {};
12
+ const keys = Object.keys(obj).sort();
13
+ for (const key of keys) {
14
+ sorted[key] = sortObjectKeys(obj[key]);
15
+ }
16
+ return sorted;
17
+ }
18
+ /**
19
+ * Compute a deterministic hash for animation props.
20
+ * This hash is used to look up pre-rendered assets at runtime.
21
+ *
22
+ * Note: This must produce the same hash as the build-time hasher.
23
+ * Uses the djb2 algorithm for cross-environment compatibility.
24
+ */
25
+ export function computePropsHash(scene, props) {
26
+ // Create a deterministic string representation
27
+ // Sort keys to ensure consistent ordering
28
+ const sortedProps = sortObjectKeys(props);
29
+ const data = JSON.stringify({ scene, props: sortedProps });
30
+ // Use djb2 hash algorithm - matches the build-time implementation
31
+ let hash = 5381;
32
+ for (let i = 0; i < data.length; i++) {
33
+ hash = ((hash << 5) + hash + data.charCodeAt(i)) | 0;
34
+ }
35
+ // Convert to positive hex string, padded to 8 chars
36
+ const hexHash = (hash >>> 0).toString(16).padStart(8, "0");
37
+ return hexHash;
38
+ }
39
+ /**
40
+ * Extract text content from React children.
41
+ * Handles strings and arrays of strings.
42
+ */
43
+ export function extractChildrenText(children) {
44
+ if (typeof children === "string") {
45
+ return children.trim();
46
+ }
47
+ if (typeof children === "number") {
48
+ return String(children);
49
+ }
50
+ if (Array.isArray(children)) {
51
+ const texts = children
52
+ .map((child) => {
53
+ if (typeof child === "string")
54
+ return child.trim();
55
+ if (typeof child === "number")
56
+ return String(child);
57
+ return "";
58
+ })
59
+ .filter(Boolean);
60
+ return texts.length > 0 ? texts.join(" ") : null;
61
+ }
62
+ return null;
63
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { computePropsHash, extractChildrenText } from "./hash";
3
+ describe("computePropsHash", () => {
4
+ it("should produce consistent hashes for same input", () => {
5
+ const hash1 = computePropsHash("TextScene", { text: "Hello", fontSize: 72 });
6
+ const hash2 = computePropsHash("TextScene", { text: "Hello", fontSize: 72 });
7
+ expect(hash1).toBe(hash2);
8
+ });
9
+ it("should produce different hashes for different scenes", () => {
10
+ const hash1 = computePropsHash("TextScene", { text: "Hello" });
11
+ const hash2 = computePropsHash("CustomScene", { text: "Hello" });
12
+ expect(hash1).not.toBe(hash2);
13
+ });
14
+ it("should produce different hashes for different props", () => {
15
+ const hash1 = computePropsHash("TextScene", { text: "Hello" });
16
+ const hash2 = computePropsHash("TextScene", { text: "World" });
17
+ expect(hash1).not.toBe(hash2);
18
+ });
19
+ it("should normalize key order (produce same hash regardless of key order)", () => {
20
+ const hash1 = computePropsHash("TextScene", { a: 1, b: 2, c: 3 });
21
+ const hash2 = computePropsHash("TextScene", { c: 3, a: 1, b: 2 });
22
+ expect(hash1).toBe(hash2);
23
+ });
24
+ it("should handle empty props", () => {
25
+ const hash = computePropsHash("TextScene", {});
26
+ expect(hash).toHaveLength(8);
27
+ expect(hash).toMatch(/^[0-9a-f]{8}$/);
28
+ });
29
+ it("should handle nested objects", () => {
30
+ const hash1 = computePropsHash("Scene", { config: { theme: "dark", size: 10 } });
31
+ const hash2 = computePropsHash("Scene", { config: { size: 10, theme: "dark" } });
32
+ expect(hash1).toBe(hash2);
33
+ });
34
+ it("should handle arrays in props", () => {
35
+ const hash1 = computePropsHash("Scene", { items: [1, 2, 3] });
36
+ const hash2 = computePropsHash("Scene", { items: [1, 2, 3] });
37
+ expect(hash1).toBe(hash2);
38
+ // Different array order should produce different hash
39
+ const hash3 = computePropsHash("Scene", { items: [3, 2, 1] });
40
+ expect(hash1).not.toBe(hash3);
41
+ });
42
+ it("should handle null values", () => {
43
+ const hash = computePropsHash("Scene", { value: null });
44
+ expect(hash).toHaveLength(8);
45
+ });
46
+ it("should handle boolean values", () => {
47
+ const hash1 = computePropsHash("Scene", { enabled: true });
48
+ const hash2 = computePropsHash("Scene", { enabled: false });
49
+ expect(hash1).not.toBe(hash2);
50
+ });
51
+ it("should handle string values", () => {
52
+ const hash1 = computePropsHash("Scene", { color: "#ffffff" });
53
+ const hash2 = computePropsHash("Scene", { color: "#000000" });
54
+ expect(hash1).not.toBe(hash2);
55
+ });
56
+ it("should handle number values", () => {
57
+ const hash1 = computePropsHash("Scene", { size: 72 });
58
+ const hash2 = computePropsHash("Scene", { size: 48 });
59
+ expect(hash1).not.toBe(hash2);
60
+ });
61
+ it("should produce valid hex string", () => {
62
+ const hash = computePropsHash("TextScene", { text: "Test", fontSize: 72 });
63
+ expect(hash).toMatch(/^[0-9a-f]{8}$/);
64
+ });
65
+ it("should handle unicode characters", () => {
66
+ const hash1 = computePropsHash("Scene", { text: "Hello 👋 World" });
67
+ const hash2 = computePropsHash("Scene", { text: "Hello 👋 World" });
68
+ expect(hash1).toBe(hash2);
69
+ });
70
+ it("should handle special characters", () => {
71
+ const hash = computePropsHash("Scene", { text: "Hello\n\t\"World\"" });
72
+ expect(hash).toHaveLength(8);
73
+ });
74
+ });
75
+ describe("extractChildrenText", () => {
76
+ it("should extract string children", () => {
77
+ expect(extractChildrenText("Hello World")).toBe("Hello World");
78
+ });
79
+ it("should trim string children", () => {
80
+ expect(extractChildrenText(" Hello World ")).toBe("Hello World");
81
+ });
82
+ it("should convert number children to string", () => {
83
+ expect(extractChildrenText(42)).toBe("42");
84
+ expect(extractChildrenText(3.14)).toBe("3.14");
85
+ expect(extractChildrenText(0)).toBe("0");
86
+ });
87
+ it("should extract text from array of strings", () => {
88
+ expect(extractChildrenText(["Hello", " ", "World"])).toBe("Hello World");
89
+ });
90
+ it("should handle array with numbers", () => {
91
+ expect(extractChildrenText(["Value: ", 42])).toBe("Value: 42");
92
+ });
93
+ it("should filter out empty strings from arrays", () => {
94
+ expect(extractChildrenText(["Hello", "", "World"])).toBe("Hello World");
95
+ });
96
+ it("should return null for non-extractable children", () => {
97
+ expect(extractChildrenText(null)).toBeNull();
98
+ expect(extractChildrenText(undefined)).toBeNull();
99
+ });
100
+ it("should return null for empty array", () => {
101
+ expect(extractChildrenText([])).toBeNull();
102
+ });
103
+ it("should return null for array of empty strings", () => {
104
+ expect(extractChildrenText(["", " ", ""])).toBeNull();
105
+ });
106
+ it("should handle mixed array with non-string elements", () => {
107
+ // Objects in array should be treated as empty
108
+ expect(extractChildrenText(["Hello", {}, "World"])).toBe("Hello World");
109
+ });
110
+ it("should handle objects (not extractable)", () => {
111
+ expect(extractChildrenText({})).toBeNull();
112
+ });
113
+ it("should handle boolean children", () => {
114
+ expect(extractChildrenText(true)).toBeNull();
115
+ expect(extractChildrenText(false)).toBeNull();
116
+ });
117
+ });
118
+ describe("sortObjectKeys (via computePropsHash)", () => {
119
+ // Test the sortObjectKeys function indirectly through computePropsHash
120
+ it("should handle deeply nested objects", () => {
121
+ const props1 = {
122
+ level1: {
123
+ level2: {
124
+ level3: {
125
+ b: 2,
126
+ a: 1,
127
+ },
128
+ },
129
+ },
130
+ };
131
+ const props2 = {
132
+ level1: {
133
+ level2: {
134
+ level3: {
135
+ a: 1,
136
+ b: 2,
137
+ },
138
+ },
139
+ },
140
+ };
141
+ const hash1 = computePropsHash("Scene", props1);
142
+ const hash2 = computePropsHash("Scene", props2);
143
+ expect(hash1).toBe(hash2);
144
+ });
145
+ it("should handle arrays of objects", () => {
146
+ const props1 = {
147
+ items: [
148
+ { z: 3, y: 2, x: 1 },
149
+ { c: 3, b: 2, a: 1 },
150
+ ],
151
+ };
152
+ const props2 = {
153
+ items: [
154
+ { x: 1, y: 2, z: 3 },
155
+ { a: 1, b: 2, c: 3 },
156
+ ],
157
+ };
158
+ const hash1 = computePropsHash("Scene", props1);
159
+ const hash2 = computePropsHash("Scene", props2);
160
+ expect(hash1).toBe(hash2);
161
+ });
162
+ it("should preserve array order", () => {
163
+ const hash1 = computePropsHash("Scene", { items: ["a", "b", "c"] });
164
+ const hash2 = computePropsHash("Scene", { items: ["c", "b", "a"] });
165
+ expect(hash1).not.toBe(hash2);
166
+ });
167
+ it("should handle null values in nested objects", () => {
168
+ const hash = computePropsHash("Scene", {
169
+ config: {
170
+ value: null,
171
+ other: "test",
172
+ },
173
+ });
174
+ expect(hash).toHaveLength(8);
175
+ });
176
+ });
@@ -0,0 +1,82 @@
1
+ import type { ScrollRangeValue } from "@mihirsarya/manim-scroll-runtime";
2
+ interface CacheManifest {
3
+ version: number;
4
+ animations: Record<string, string>;
5
+ }
6
+ /**
7
+ * Load the cache manifest from the public directory.
8
+ */
9
+ declare function loadCacheManifest(): Promise<CacheManifest | null>;
10
+ /**
11
+ * Resolve the manifest URL for the given animation props.
12
+ */
13
+ declare function resolveManifestUrl(scene: string, props: Record<string, unknown>): Promise<string | null>;
14
+ /**
15
+ * Options for the useManimScroll hook.
16
+ */
17
+ export interface UseManimScrollOptions {
18
+ /** Ref to the container element */
19
+ ref: React.RefObject<HTMLElement | null>;
20
+ /** Explicit manifest URL (skips auto-resolution) */
21
+ manifestUrl?: string;
22
+ /** Scene name for auto-resolution (default: "TextScene") */
23
+ scene?: string;
24
+ /** Animation props for auto-resolution (hashed to find cached assets) */
25
+ animationProps?: Record<string, unknown>;
26
+ /** Playback mode */
27
+ mode?: "auto" | "frames" | "video";
28
+ /** Scroll range configuration */
29
+ scrollRange?: ScrollRangeValue;
30
+ /** Canvas dimensions */
31
+ canvasDimensions?: {
32
+ width?: number;
33
+ height?: number;
34
+ };
35
+ }
36
+ /**
37
+ * Result returned by the useManimScroll hook.
38
+ */
39
+ export interface UseManimScrollResult {
40
+ /** Current scroll progress (0 to 1) */
41
+ progress: number;
42
+ /** Whether the animation is loaded and ready */
43
+ isReady: boolean;
44
+ /** Error if loading/resolution failed */
45
+ error: Error | null;
46
+ /** Ref to attach to an optional canvas element */
47
+ canvasRef: React.RefObject<HTMLCanvasElement | null>;
48
+ /** Imperatively seek to a specific progress (0 to 1) */
49
+ seek: (progress: number) => void;
50
+ /** Pause scroll-driven updates */
51
+ pause: () => void;
52
+ /** Resume scroll-driven updates */
53
+ resume: () => void;
54
+ /** Whether scroll updates are paused */
55
+ isPaused: boolean;
56
+ }
57
+ /**
58
+ * Hook for advanced scroll-driven Manim animation control.
59
+ *
60
+ * Provides fine-grained control over the animation lifecycle, progress tracking,
61
+ * and imperative methods for seeking and pausing.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * function CustomAnimation() {
66
+ * const containerRef = useRef<HTMLDivElement>(null);
67
+ * const { progress, isReady, error } = useManimScroll({
68
+ * ref: containerRef,
69
+ * manifestUrl: "/assets/scene/manifest.json",
70
+ * });
71
+ *
72
+ * return (
73
+ * <div ref={containerRef}>
74
+ * {!isReady && <Spinner />}
75
+ * <ProgressBar value={progress} />
76
+ * </div>
77
+ * );
78
+ * }
79
+ * ```
80
+ */
81
+ export declare function useManimScroll(options: UseManimScrollOptions): UseManimScrollResult;
82
+ export { loadCacheManifest, resolveManifestUrl };
package/dist/hooks.js ADDED
@@ -0,0 +1,182 @@
1
+ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
2
+ import { registerScrollAnimation } from "@mihirsarya/manim-scroll-runtime";
3
+ import { computePropsHash } from "./hash";
4
+ // Global cache manifest (loaded once)
5
+ let cacheManifestPromise = null;
6
+ /**
7
+ * Load the cache manifest from the public directory.
8
+ */
9
+ async function loadCacheManifest() {
10
+ if (cacheManifestPromise) {
11
+ return cacheManifestPromise;
12
+ }
13
+ cacheManifestPromise = fetch("/manim-assets/cache-manifest.json")
14
+ .then((response) => {
15
+ if (!response.ok) {
16
+ return null;
17
+ }
18
+ return response.json();
19
+ })
20
+ .catch(() => null);
21
+ return cacheManifestPromise;
22
+ }
23
+ /**
24
+ * Resolve the manifest URL for the given animation props.
25
+ */
26
+ async function resolveManifestUrl(scene, props) {
27
+ var _a;
28
+ const manifest = await loadCacheManifest();
29
+ if (!manifest) {
30
+ return null;
31
+ }
32
+ const hash = computePropsHash(scene, props);
33
+ return (_a = manifest.animations[hash]) !== null && _a !== void 0 ? _a : null;
34
+ }
35
+ /**
36
+ * Hook for advanced scroll-driven Manim animation control.
37
+ *
38
+ * Provides fine-grained control over the animation lifecycle, progress tracking,
39
+ * and imperative methods for seeking and pausing.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * function CustomAnimation() {
44
+ * const containerRef = useRef<HTMLDivElement>(null);
45
+ * const { progress, isReady, error } = useManimScroll({
46
+ * ref: containerRef,
47
+ * manifestUrl: "/assets/scene/manifest.json",
48
+ * });
49
+ *
50
+ * return (
51
+ * <div ref={containerRef}>
52
+ * {!isReady && <Spinner />}
53
+ * <ProgressBar value={progress} />
54
+ * </div>
55
+ * );
56
+ * }
57
+ * ```
58
+ */
59
+ export function useManimScroll(options) {
60
+ const { ref, manifestUrl: explicitManifestUrl, scene = "TextScene", animationProps = {}, mode, scrollRange, canvasDimensions, } = options;
61
+ const [progress, setProgress] = useState(0);
62
+ const [isReady, setIsReady] = useState(false);
63
+ const [error, setError] = useState(null);
64
+ const [isPaused, setIsPaused] = useState(false);
65
+ const [resolvedManifestUrl, setResolvedManifestUrl] = useState(explicitManifestUrl !== null && explicitManifestUrl !== void 0 ? explicitManifestUrl : null);
66
+ const canvasRef = useRef(null);
67
+ const cleanupRef = useRef(null);
68
+ const pausedRef = useRef(isPaused);
69
+ // Keep pausedRef in sync
70
+ useEffect(() => {
71
+ pausedRef.current = isPaused;
72
+ }, [isPaused]);
73
+ // Memoize animation props for stable dependency
74
+ const memoizedAnimationProps = useMemo(() => animationProps,
75
+ // eslint-disable-next-line react-hooks/exhaustive-deps
76
+ [JSON.stringify(animationProps)]);
77
+ // Resolve manifest URL if not explicitly provided
78
+ useEffect(() => {
79
+ if (explicitManifestUrl) {
80
+ setResolvedManifestUrl(explicitManifestUrl);
81
+ setError(null);
82
+ return;
83
+ }
84
+ resolveManifestUrl(scene, memoizedAnimationProps).then((url) => {
85
+ if (url) {
86
+ setResolvedManifestUrl(url);
87
+ setError(null);
88
+ }
89
+ else {
90
+ setError(new Error(`No pre-rendered animation found for scene "${scene}". ` +
91
+ `Make sure to run the build with @mihirsarya/manim-scroll-next.`));
92
+ }
93
+ });
94
+ }, [explicitManifestUrl, scene, memoizedAnimationProps]);
95
+ // Handle progress updates (respects pause state)
96
+ const handleProgress = useCallback((p) => {
97
+ if (!pausedRef.current) {
98
+ setProgress(p);
99
+ }
100
+ }, []);
101
+ // Handle ready callback
102
+ const handleReady = useCallback(() => {
103
+ setIsReady(true);
104
+ }, []);
105
+ // Register scroll animation
106
+ useEffect(() => {
107
+ var _a;
108
+ const container = ref.current;
109
+ if (!container || !resolvedManifestUrl)
110
+ return;
111
+ let isMounted = true;
112
+ // Create or use existing canvas
113
+ const canvasEl = (_a = canvasRef.current) !== null && _a !== void 0 ? _a : document.createElement("canvas");
114
+ if (canvasDimensions === null || canvasDimensions === void 0 ? void 0 : canvasDimensions.width)
115
+ canvasEl.width = canvasDimensions.width;
116
+ if (canvasDimensions === null || canvasDimensions === void 0 ? void 0 : canvasDimensions.height)
117
+ canvasEl.height = canvasDimensions.height;
118
+ // Only append if we created the canvas
119
+ if (!canvasRef.current) {
120
+ container.appendChild(canvasEl);
121
+ }
122
+ registerScrollAnimation({
123
+ manifestUrl: resolvedManifestUrl,
124
+ mode,
125
+ scrollRange,
126
+ onReady: handleReady,
127
+ onProgress: handleProgress,
128
+ container,
129
+ canvas: canvasEl,
130
+ }).then((dispose) => {
131
+ if (!isMounted) {
132
+ dispose();
133
+ return;
134
+ }
135
+ cleanupRef.current = dispose;
136
+ });
137
+ return () => {
138
+ var _a;
139
+ isMounted = false;
140
+ (_a = cleanupRef.current) === null || _a === void 0 ? void 0 : _a.call(cleanupRef);
141
+ // Only remove if we created the canvas
142
+ if (!canvasRef.current) {
143
+ canvasEl.remove();
144
+ }
145
+ setIsReady(false);
146
+ };
147
+ }, [
148
+ ref,
149
+ resolvedManifestUrl,
150
+ mode,
151
+ scrollRange,
152
+ canvasDimensions === null || canvasDimensions === void 0 ? void 0 : canvasDimensions.width,
153
+ canvasDimensions === null || canvasDimensions === void 0 ? void 0 : canvasDimensions.height,
154
+ handleProgress,
155
+ handleReady,
156
+ ]);
157
+ // Imperative methods
158
+ const seek = useCallback((targetProgress) => {
159
+ const clamped = Math.min(1, Math.max(0, targetProgress));
160
+ setProgress(clamped);
161
+ // Note: actual seeking in the player would require extending ScrollPlayer
162
+ // For now, this updates the progress state for UI synchronization
163
+ }, []);
164
+ const pause = useCallback(() => {
165
+ setIsPaused(true);
166
+ }, []);
167
+ const resume = useCallback(() => {
168
+ setIsPaused(false);
169
+ }, []);
170
+ return {
171
+ progress,
172
+ isReady,
173
+ error,
174
+ canvasRef,
175
+ seek,
176
+ pause,
177
+ resume,
178
+ isPaused,
179
+ };
180
+ }
181
+ // Re-export helper functions for use by the component
182
+ export { loadCacheManifest, resolveManifestUrl };
@@ -0,0 +1,5 @@
1
+ export { ManimScroll } from "./ManimScroll";
2
+ export type { ManimScrollProps, ManimAnimationProps } from "./ManimScroll";
3
+ export { useManimScroll } from "./hooks";
4
+ export type { UseManimScrollOptions, UseManimScrollResult } from "./hooks";
5
+ export { computePropsHash, extractChildrenText } from "./hash";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ManimScroll } from "./ManimScroll";
2
+ export { useManimScroll } from "./hooks";
3
+ export { computePropsHash, extractChildrenText } from "./hash";
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@mihirsarya/manim-scroll-react",
3
+ "version": "0.1.1",
4
+ "description": "React wrapper for scroll-driven Manim animations.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "peerDependencies": {
11
+ "react": ">=18.0.0",
12
+ "react-dom": ">=18.0.0"
13
+ },
14
+ "dependencies": {
15
+ "@mihirsarya/manim-scroll-runtime": "0.1.1"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.2.61",
19
+ "@types/react-dom": "^18.2.19",
20
+ "typescript": "^5.4.5"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json"
24
+ }
25
+ }