@mihirsarya/manim-scroll-react 0.1.2 → 0.2.0
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/dist/ManimScroll.d.ts +27 -4
- package/dist/ManimScroll.js +60 -9
- package/dist/hooks.d.ts +71 -2
- package/dist/hooks.js +166 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/ManimScroll.d.ts
CHANGED
|
@@ -7,20 +7,33 @@ import type { ScrollRangeValue } from "@mihirsarya/manim-scroll-runtime";
|
|
|
7
7
|
export interface ManimAnimationProps {
|
|
8
8
|
/** Scene name (default: "TextScene") */
|
|
9
9
|
scene?: string;
|
|
10
|
-
/** Font size for text animations */
|
|
10
|
+
/** Font size for text animations. If not specified in native mode, inherits from parent. */
|
|
11
11
|
fontSize?: number;
|
|
12
12
|
/** Color for the animation (hex string) */
|
|
13
13
|
color?: string;
|
|
14
14
|
/** Font family for text animations */
|
|
15
15
|
font?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Enable inline mode for text that flows with surrounding content.
|
|
18
|
+
* When true, renders with transparent background and tight bounds.
|
|
19
|
+
*/
|
|
20
|
+
inline?: boolean;
|
|
21
|
+
/** Padding around the text in inline mode (Manim units, default: 0.2) */
|
|
22
|
+
padding?: number;
|
|
16
23
|
/** Additional custom props for the scene */
|
|
17
24
|
[key: string]: unknown;
|
|
18
25
|
}
|
|
19
26
|
export type ManimScrollProps = ManimAnimationProps & {
|
|
20
27
|
/** Explicit manifest URL (overrides auto-resolution) */
|
|
21
28
|
manifestUrl?: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Playback mode:
|
|
31
|
+
* - "auto": Uses pre-rendered video or frames
|
|
32
|
+
* - "frames": Forces frame-by-frame playback
|
|
33
|
+
* - "video": Forces video playback
|
|
34
|
+
* - "native": Uses native SVG animation (no pre-rendered assets)
|
|
35
|
+
*/
|
|
36
|
+
mode?: "auto" | "frames" | "video" | "native";
|
|
24
37
|
/** Scroll range configuration (preset, tuple, or legacy object) */
|
|
25
38
|
scrollRange?: ScrollRangeValue;
|
|
26
39
|
/** Called when animation is loaded and ready */
|
|
@@ -32,6 +45,10 @@ export type ManimScrollProps = ManimAnimationProps & {
|
|
|
32
45
|
width?: number;
|
|
33
46
|
height?: number;
|
|
34
47
|
};
|
|
48
|
+
/** URL to a font file for native mode (woff, woff2, ttf, otf) */
|
|
49
|
+
fontUrl?: string;
|
|
50
|
+
/** Stroke width for native mode drawing phase */
|
|
51
|
+
strokeWidth?: number;
|
|
35
52
|
className?: string;
|
|
36
53
|
style?: React.CSSProperties;
|
|
37
54
|
children?: React.ReactNode;
|
|
@@ -41,6 +58,7 @@ export type ManimScrollProps = ManimAnimationProps & {
|
|
|
41
58
|
*
|
|
42
59
|
* Automatically resolves pre-rendered animation assets based on props,
|
|
43
60
|
* or use an explicit `manifestUrl` for manual control.
|
|
61
|
+
* Use `mode="native"` for native SVG animation without pre-rendered assets.
|
|
44
62
|
*
|
|
45
63
|
* @example
|
|
46
64
|
* ```tsx
|
|
@@ -53,6 +71,11 @@ export type ManimScrollProps = ManimAnimationProps & {
|
|
|
53
71
|
* <ManimScroll manifestUrl="/assets/scene/manifest.json">
|
|
54
72
|
* Scroll-driven text
|
|
55
73
|
* </ManimScroll>
|
|
74
|
+
*
|
|
75
|
+
* // Native mode (no pre-rendered assets)
|
|
76
|
+
* <ManimScroll mode="native" fontSize={48} color="#ffffff">
|
|
77
|
+
* Animate this text
|
|
78
|
+
* </ManimScroll>
|
|
56
79
|
* ```
|
|
57
80
|
*/
|
|
58
|
-
export declare function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, scene, fontSize, color, font, ...customProps }: ManimScrollProps): JSX.Element;
|
|
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;
|
package/dist/ManimScroll.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useMemo, useEffect } from "react";
|
|
3
|
-
import { useManimScroll } from "./hooks";
|
|
3
|
+
import { useManimScroll, useNativeAnimation } from "./hooks";
|
|
4
4
|
import { extractChildrenText } from "./hash";
|
|
5
5
|
/**
|
|
6
6
|
* Scroll-driven Manim animation component.
|
|
7
7
|
*
|
|
8
8
|
* Automatically resolves pre-rendered animation assets based on props,
|
|
9
9
|
* or use an explicit `manifestUrl` for manual control.
|
|
10
|
+
* Use `mode="native"` for native SVG animation without pre-rendered assets.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```tsx
|
|
@@ -19,15 +20,22 @@ import { extractChildrenText } from "./hash";
|
|
|
19
20
|
* <ManimScroll manifestUrl="/assets/scene/manifest.json">
|
|
20
21
|
* Scroll-driven text
|
|
21
22
|
* </ManimScroll>
|
|
23
|
+
*
|
|
24
|
+
* // Native mode (no pre-rendered assets)
|
|
25
|
+
* <ManimScroll mode="native" fontSize={48} color="#ffffff">
|
|
26
|
+
* Animate this text
|
|
27
|
+
* </ManimScroll>
|
|
22
28
|
* ```
|
|
23
29
|
*/
|
|
24
|
-
export function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress,
|
|
30
|
+
export function ManimScroll({ className, style, children, canvas, manifestUrl, mode, scrollRange, onReady, onProgress, fontUrl, strokeWidth,
|
|
25
31
|
// Animation props
|
|
26
|
-
scene = "TextScene", fontSize, color, font, ...customProps }) {
|
|
32
|
+
scene = "TextScene", fontSize, color, font, inline, padding, ...customProps }) {
|
|
27
33
|
const containerRef = useRef(null);
|
|
28
34
|
// Extract text from children
|
|
29
35
|
const childrenText = useMemo(() => extractChildrenText(children), [children]);
|
|
30
|
-
//
|
|
36
|
+
// Determine if we're using native mode
|
|
37
|
+
const isNativeMode = mode === "native";
|
|
38
|
+
// Build animation props for hashing (used for pre-rendered mode)
|
|
31
39
|
const animationProps = useMemo(() => {
|
|
32
40
|
const props = { ...customProps };
|
|
33
41
|
if (childrenText)
|
|
@@ -38,18 +46,36 @@ scene = "TextScene", fontSize, color, font, ...customProps }) {
|
|
|
38
46
|
props.color = color;
|
|
39
47
|
if (font !== undefined)
|
|
40
48
|
props.font = font;
|
|
49
|
+
if (inline !== undefined)
|
|
50
|
+
props.inline = inline;
|
|
51
|
+
if (padding !== undefined)
|
|
52
|
+
props.padding = padding;
|
|
41
53
|
return props;
|
|
42
|
-
}, [childrenText, fontSize, color, font, customProps]);
|
|
43
|
-
// Use
|
|
44
|
-
|
|
54
|
+
}, [childrenText, fontSize, color, font, inline, padding, customProps]);
|
|
55
|
+
// Use appropriate hook based on mode
|
|
56
|
+
// Only one hook will be active at a time based on the enabled flag
|
|
57
|
+
const preRenderedResult = useManimScroll({
|
|
45
58
|
ref: containerRef,
|
|
46
59
|
manifestUrl,
|
|
47
60
|
scene,
|
|
48
61
|
animationProps,
|
|
49
|
-
mode,
|
|
62
|
+
mode: isNativeMode ? undefined : mode,
|
|
50
63
|
scrollRange,
|
|
51
64
|
canvasDimensions: canvas,
|
|
65
|
+
enabled: !isNativeMode,
|
|
66
|
+
});
|
|
67
|
+
const nativeResult = useNativeAnimation({
|
|
68
|
+
ref: containerRef,
|
|
69
|
+
text: childrenText || "",
|
|
70
|
+
fontSize, // undefined means inherit from parent
|
|
71
|
+
color: color !== null && color !== void 0 ? color : "#ffffff",
|
|
72
|
+
fontUrl,
|
|
73
|
+
strokeWidth,
|
|
74
|
+
scrollRange,
|
|
75
|
+
enabled: isNativeMode,
|
|
52
76
|
});
|
|
77
|
+
// Select the appropriate result based on mode
|
|
78
|
+
const { isReady, error, progress } = isNativeMode ? nativeResult : preRenderedResult;
|
|
53
79
|
// Forward callbacks
|
|
54
80
|
useEffect(() => {
|
|
55
81
|
if (isReady) {
|
|
@@ -59,7 +85,32 @@ scene = "TextScene", fontSize, color, font, ...customProps }) {
|
|
|
59
85
|
useEffect(() => {
|
|
60
86
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress);
|
|
61
87
|
}, [progress, onProgress]);
|
|
62
|
-
|
|
88
|
+
// Compute container styles based on inline mode and native mode
|
|
89
|
+
const containerStyle = useMemo(() => {
|
|
90
|
+
const baseStyle = {
|
|
91
|
+
position: "relative",
|
|
92
|
+
...style,
|
|
93
|
+
};
|
|
94
|
+
if (inline || isNativeMode) {
|
|
95
|
+
return {
|
|
96
|
+
...baseStyle,
|
|
97
|
+
// Use inline display for natural text flow
|
|
98
|
+
display: "inline",
|
|
99
|
+
// Align with surrounding text baseline for seamless integration
|
|
100
|
+
verticalAlign: "baseline",
|
|
101
|
+
// Ensure background is transparent for inline mode
|
|
102
|
+
background: "transparent",
|
|
103
|
+
// Inherit font properties for seamless text flow
|
|
104
|
+
fontFamily: "inherit",
|
|
105
|
+
lineHeight: "inherit",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return baseStyle;
|
|
109
|
+
}, [inline, isNativeMode, style]);
|
|
110
|
+
// Use span for inline mode or native mode to avoid invalid HTML (div inside p)
|
|
111
|
+
// Native mode typically flows inline with text, so use span for proper semantics
|
|
112
|
+
const Container = inline || isNativeMode ? "span" : "div";
|
|
113
|
+
return (_jsxs(Container, { className: className, style: containerStyle, ref: containerRef, children: [!isNativeMode && (_jsx("span", { style: { position: "absolute", opacity: 0 }, children: children })), error && process.env.NODE_ENV === "development" && (_jsx("span", { style: {
|
|
63
114
|
position: "absolute",
|
|
64
115
|
inset: 0,
|
|
65
116
|
display: "flex",
|
package/dist/hooks.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ interface CacheManifest {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Load the cache manifest from the public directory.
|
|
8
|
+
* In dev mode, refreshes the manifest periodically to pick up newly rendered animations.
|
|
8
9
|
*/
|
|
9
|
-
declare function loadCacheManifest(): Promise<CacheManifest | null>;
|
|
10
|
+
declare function loadCacheManifest(forceRefresh?: boolean): Promise<CacheManifest | null>;
|
|
10
11
|
/**
|
|
11
12
|
* Resolve the manifest URL for the given animation props.
|
|
13
|
+
* In dev mode, retries with manifest refresh if animation not found.
|
|
12
14
|
*/
|
|
13
|
-
declare function resolveManifestUrl(scene: string, props: Record<string, unknown
|
|
15
|
+
declare function resolveManifestUrl(scene: string, props: Record<string, unknown>, retryCount?: number): Promise<string | null>;
|
|
14
16
|
/**
|
|
15
17
|
* Options for the useManimScroll hook.
|
|
16
18
|
*/
|
|
@@ -32,6 +34,8 @@ export interface UseManimScrollOptions {
|
|
|
32
34
|
width?: number;
|
|
33
35
|
height?: number;
|
|
34
36
|
};
|
|
37
|
+
/** Whether the hook is enabled (default: true) */
|
|
38
|
+
enabled?: boolean;
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
41
|
* Result returned by the useManimScroll hook.
|
|
@@ -80,3 +84,68 @@ export interface UseManimScrollResult {
|
|
|
80
84
|
*/
|
|
81
85
|
export declare function useManimScroll(options: UseManimScrollOptions): UseManimScrollResult;
|
|
82
86
|
export { loadCacheManifest, resolveManifestUrl };
|
|
87
|
+
/**
|
|
88
|
+
* Options for the useNativeAnimation hook.
|
|
89
|
+
*/
|
|
90
|
+
export interface UseNativeAnimationOptions {
|
|
91
|
+
/** Ref to the container element */
|
|
92
|
+
ref: React.RefObject<HTMLElement | null>;
|
|
93
|
+
/** The text to animate */
|
|
94
|
+
text: string;
|
|
95
|
+
/** Font size in pixels. If not specified, inherits from parent element. */
|
|
96
|
+
fontSize?: number;
|
|
97
|
+
/** Text color (hex or CSS color) */
|
|
98
|
+
color?: string;
|
|
99
|
+
/** URL to a font file (woff, woff2, ttf, otf) for opentype.js */
|
|
100
|
+
fontUrl?: string;
|
|
101
|
+
/** Stroke width for the drawing phase */
|
|
102
|
+
strokeWidth?: number;
|
|
103
|
+
/** Scroll range configuration */
|
|
104
|
+
scrollRange?: ScrollRangeValue;
|
|
105
|
+
/** Whether the hook is enabled (default: true) */
|
|
106
|
+
enabled?: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Result returned by the useNativeAnimation hook.
|
|
110
|
+
*/
|
|
111
|
+
export interface UseNativeAnimationResult {
|
|
112
|
+
/** Current scroll progress (0 to 1) */
|
|
113
|
+
progress: number;
|
|
114
|
+
/** Whether the animation is loaded and ready */
|
|
115
|
+
isReady: boolean;
|
|
116
|
+
/** Error if initialization failed */
|
|
117
|
+
error: Error | null;
|
|
118
|
+
/** Pause scroll-driven updates */
|
|
119
|
+
pause: () => void;
|
|
120
|
+
/** Resume scroll-driven updates */
|
|
121
|
+
resume: () => void;
|
|
122
|
+
/** Whether scroll updates are paused */
|
|
123
|
+
isPaused: boolean;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Hook for native text animation that renders directly in the browser
|
|
127
|
+
* using SVG, replicating Manim's Write/DrawBorderThenFill effect.
|
|
128
|
+
*
|
|
129
|
+
* This hook bypasses the manifest resolution and pre-rendered assets,
|
|
130
|
+
* instead animating text natively using opentype.js and SVG.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```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
|
+
* });
|
|
142
|
+
*
|
|
143
|
+
* return (
|
|
144
|
+
* <div ref={containerRef} style={{ height: "100vh" }}>
|
|
145
|
+
* {!isReady && <div>Loading...</div>}
|
|
146
|
+
* </div>
|
|
147
|
+
* );
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export declare function useNativeAnimation(options: UseNativeAnimationOptions): UseNativeAnimationResult;
|
package/dist/hooks.js
CHANGED
|
@@ -1,36 +1,68 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
|
2
|
-
import { registerScrollAnimation } from "@mihirsarya/manim-scroll-runtime";
|
|
2
|
+
import { registerScrollAnimation, registerNativeAnimation } from "@mihirsarya/manim-scroll-runtime";
|
|
3
3
|
import { computePropsHash } from "./hash";
|
|
4
|
-
// Global cache manifest
|
|
5
|
-
let
|
|
4
|
+
// Global cache manifest state
|
|
5
|
+
let cachedManifest = null;
|
|
6
|
+
let manifestFetchPromise = null;
|
|
7
|
+
let lastFetchTime = 0;
|
|
8
|
+
// In dev mode, allow manifest to be refreshed every 2 seconds
|
|
9
|
+
const MANIFEST_CACHE_TTL = process.env.NODE_ENV === "development" ? 2000 : Infinity;
|
|
6
10
|
/**
|
|
7
11
|
* Load the cache manifest from the public directory.
|
|
12
|
+
* In dev mode, refreshes the manifest periodically to pick up newly rendered animations.
|
|
8
13
|
*/
|
|
9
|
-
async function loadCacheManifest() {
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
async function loadCacheManifest(forceRefresh = false) {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const isStale = now - lastFetchTime > MANIFEST_CACHE_TTL;
|
|
17
|
+
// Return cached manifest if fresh
|
|
18
|
+
if (cachedManifest && !isStale && !forceRefresh) {
|
|
19
|
+
return cachedManifest;
|
|
12
20
|
}
|
|
13
|
-
|
|
21
|
+
// If already fetching, wait for that
|
|
22
|
+
if (manifestFetchPromise && !forceRefresh) {
|
|
23
|
+
return manifestFetchPromise;
|
|
24
|
+
}
|
|
25
|
+
lastFetchTime = now;
|
|
26
|
+
manifestFetchPromise = fetch("/manim-assets/cache-manifest.json", {
|
|
27
|
+
cache: forceRefresh ? "no-store" : "default",
|
|
28
|
+
})
|
|
14
29
|
.then((response) => {
|
|
15
30
|
if (!response.ok) {
|
|
16
31
|
return null;
|
|
17
32
|
}
|
|
18
33
|
return response.json();
|
|
19
34
|
})
|
|
20
|
-
.
|
|
21
|
-
|
|
35
|
+
.then((manifest) => {
|
|
36
|
+
cachedManifest = manifest;
|
|
37
|
+
manifestFetchPromise = null;
|
|
38
|
+
return manifest;
|
|
39
|
+
})
|
|
40
|
+
.catch(() => {
|
|
41
|
+
manifestFetchPromise = null;
|
|
42
|
+
return null;
|
|
43
|
+
});
|
|
44
|
+
return manifestFetchPromise;
|
|
22
45
|
}
|
|
23
46
|
/**
|
|
24
47
|
* Resolve the manifest URL for the given animation props.
|
|
48
|
+
* In dev mode, retries with manifest refresh if animation not found.
|
|
25
49
|
*/
|
|
26
|
-
async function resolveManifestUrl(scene, props) {
|
|
27
|
-
|
|
28
|
-
const manifest = await loadCacheManifest();
|
|
50
|
+
async function resolveManifestUrl(scene, props, retryCount = 0) {
|
|
51
|
+
const manifest = await loadCacheManifest(retryCount > 0);
|
|
29
52
|
if (!manifest) {
|
|
30
53
|
return null;
|
|
31
54
|
}
|
|
32
55
|
const hash = computePropsHash(scene, props);
|
|
33
|
-
|
|
56
|
+
const url = manifest.animations[hash];
|
|
57
|
+
if (url) {
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
// In dev mode, retry a few times as animations might still be rendering
|
|
61
|
+
if (process.env.NODE_ENV === "development" && retryCount < 5) {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (retryCount + 1)));
|
|
63
|
+
return resolveManifestUrl(scene, props, retryCount + 1);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
34
66
|
}
|
|
35
67
|
/**
|
|
36
68
|
* Hook for advanced scroll-driven Manim animation control.
|
|
@@ -57,7 +89,7 @@ async function resolveManifestUrl(scene, props) {
|
|
|
57
89
|
* ```
|
|
58
90
|
*/
|
|
59
91
|
export function useManimScroll(options) {
|
|
60
|
-
const { ref, manifestUrl: explicitManifestUrl, scene = "TextScene", animationProps = {}, mode, scrollRange, canvasDimensions, } = options;
|
|
92
|
+
const { ref, manifestUrl: explicitManifestUrl, scene = "TextScene", animationProps = {}, mode, scrollRange, canvasDimensions, enabled = true, } = options;
|
|
61
93
|
const [progress, setProgress] = useState(0);
|
|
62
94
|
const [isReady, setIsReady] = useState(false);
|
|
63
95
|
const [error, setError] = useState(null);
|
|
@@ -76,6 +108,8 @@ export function useManimScroll(options) {
|
|
|
76
108
|
[JSON.stringify(animationProps)]);
|
|
77
109
|
// Resolve manifest URL if not explicitly provided
|
|
78
110
|
useEffect(() => {
|
|
111
|
+
if (!enabled)
|
|
112
|
+
return;
|
|
79
113
|
if (explicitManifestUrl) {
|
|
80
114
|
setResolvedManifestUrl(explicitManifestUrl);
|
|
81
115
|
setError(null);
|
|
@@ -91,7 +125,7 @@ export function useManimScroll(options) {
|
|
|
91
125
|
`Make sure to run the build with @mihirsarya/manim-scroll-next.`));
|
|
92
126
|
}
|
|
93
127
|
});
|
|
94
|
-
}, [explicitManifestUrl, scene, memoizedAnimationProps]);
|
|
128
|
+
}, [enabled, explicitManifestUrl, scene, memoizedAnimationProps]);
|
|
95
129
|
// Handle progress updates (respects pause state)
|
|
96
130
|
const handleProgress = useCallback((p) => {
|
|
97
131
|
if (!pausedRef.current) {
|
|
@@ -105,6 +139,8 @@ export function useManimScroll(options) {
|
|
|
105
139
|
// Register scroll animation
|
|
106
140
|
useEffect(() => {
|
|
107
141
|
var _a;
|
|
142
|
+
if (!enabled)
|
|
143
|
+
return;
|
|
108
144
|
const container = ref.current;
|
|
109
145
|
if (!container || !resolvedManifestUrl)
|
|
110
146
|
return;
|
|
@@ -115,8 +151,18 @@ export function useManimScroll(options) {
|
|
|
115
151
|
canvasEl.width = canvasDimensions.width;
|
|
116
152
|
if (canvasDimensions === null || canvasDimensions === void 0 ? void 0 : canvasDimensions.height)
|
|
117
153
|
canvasEl.height = canvasDimensions.height;
|
|
154
|
+
// Check if inline mode
|
|
155
|
+
const isInline = animationProps.inline === true;
|
|
118
156
|
// Only append if we created the canvas
|
|
119
157
|
if (!canvasRef.current) {
|
|
158
|
+
if (!isInline) {
|
|
159
|
+
// Block mode: fill container
|
|
160
|
+
canvasEl.style.width = "100%";
|
|
161
|
+
canvasEl.style.height = "100%";
|
|
162
|
+
canvasEl.style.objectFit = "contain";
|
|
163
|
+
canvasEl.style.display = "block";
|
|
164
|
+
}
|
|
165
|
+
// For inline mode, let the player set styles based on manifest data
|
|
120
166
|
container.appendChild(canvasEl);
|
|
121
167
|
}
|
|
122
168
|
registerScrollAnimation({
|
|
@@ -145,6 +191,7 @@ export function useManimScroll(options) {
|
|
|
145
191
|
setIsReady(false);
|
|
146
192
|
};
|
|
147
193
|
}, [
|
|
194
|
+
enabled,
|
|
148
195
|
ref,
|
|
149
196
|
resolvedManifestUrl,
|
|
150
197
|
mode,
|
|
@@ -180,3 +227,107 @@ export function useManimScroll(options) {
|
|
|
180
227
|
}
|
|
181
228
|
// Re-export helper functions for use by the component
|
|
182
229
|
export { loadCacheManifest, resolveManifestUrl };
|
|
230
|
+
/**
|
|
231
|
+
* Hook for native text animation that renders directly in the browser
|
|
232
|
+
* using SVG, replicating Manim's Write/DrawBorderThenFill effect.
|
|
233
|
+
*
|
|
234
|
+
* This hook bypasses the manifest resolution and pre-rendered assets,
|
|
235
|
+
* instead animating text natively using opentype.js and SVG.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```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
|
+
* });
|
|
247
|
+
*
|
|
248
|
+
* return (
|
|
249
|
+
* <div ref={containerRef} style={{ height: "100vh" }}>
|
|
250
|
+
* {!isReady && <div>Loading...</div>}
|
|
251
|
+
* </div>
|
|
252
|
+
* );
|
|
253
|
+
* }
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export function useNativeAnimation(options) {
|
|
257
|
+
const { ref, text, fontSize, // undefined means inherit from parent
|
|
258
|
+
color = "#ffffff", fontUrl, strokeWidth = 2, // Manim's DrawBorderThenFill default
|
|
259
|
+
scrollRange, enabled = true, } = options;
|
|
260
|
+
const [progress, setProgress] = useState(0);
|
|
261
|
+
const [isReady, setIsReady] = useState(false);
|
|
262
|
+
const [error, setError] = useState(null);
|
|
263
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
264
|
+
const cleanupRef = useRef(null);
|
|
265
|
+
const pausedRef = useRef(isPaused);
|
|
266
|
+
// Keep pausedRef in sync
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
pausedRef.current = isPaused;
|
|
269
|
+
}, [isPaused]);
|
|
270
|
+
// Handle progress updates (respects pause state)
|
|
271
|
+
const handleProgress = useCallback((p) => {
|
|
272
|
+
if (!pausedRef.current) {
|
|
273
|
+
setProgress(p);
|
|
274
|
+
}
|
|
275
|
+
}, []);
|
|
276
|
+
// Handle ready callback
|
|
277
|
+
const handleReady = useCallback(() => {
|
|
278
|
+
setIsReady(true);
|
|
279
|
+
}, []);
|
|
280
|
+
// Register native animation
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (!enabled)
|
|
283
|
+
return;
|
|
284
|
+
const container = ref.current;
|
|
285
|
+
if (!container || !text)
|
|
286
|
+
return;
|
|
287
|
+
let isMounted = true;
|
|
288
|
+
const nativeOptions = {
|
|
289
|
+
container,
|
|
290
|
+
text,
|
|
291
|
+
fontSize,
|
|
292
|
+
color,
|
|
293
|
+
fontUrl,
|
|
294
|
+
strokeWidth,
|
|
295
|
+
scrollRange,
|
|
296
|
+
onReady: handleReady,
|
|
297
|
+
onProgress: handleProgress,
|
|
298
|
+
};
|
|
299
|
+
registerNativeAnimation(nativeOptions)
|
|
300
|
+
.then((dispose) => {
|
|
301
|
+
if (!isMounted) {
|
|
302
|
+
dispose();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
cleanupRef.current = dispose;
|
|
306
|
+
})
|
|
307
|
+
.catch((err) => {
|
|
308
|
+
if (isMounted) {
|
|
309
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return () => {
|
|
313
|
+
var _a;
|
|
314
|
+
isMounted = false;
|
|
315
|
+
(_a = cleanupRef.current) === null || _a === void 0 ? void 0 : _a.call(cleanupRef);
|
|
316
|
+
setIsReady(false);
|
|
317
|
+
};
|
|
318
|
+
}, [enabled, ref, text, fontSize, color, fontUrl, strokeWidth, scrollRange, handleProgress, handleReady]);
|
|
319
|
+
const pause = useCallback(() => {
|
|
320
|
+
setIsPaused(true);
|
|
321
|
+
}, []);
|
|
322
|
+
const resume = useCallback(() => {
|
|
323
|
+
setIsPaused(false);
|
|
324
|
+
}, []);
|
|
325
|
+
return {
|
|
326
|
+
progress,
|
|
327
|
+
isReady,
|
|
328
|
+
error,
|
|
329
|
+
pause,
|
|
330
|
+
resume,
|
|
331
|
+
isPaused,
|
|
332
|
+
};
|
|
333
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ManimScroll } from "./ManimScroll";
|
|
2
2
|
export type { ManimScrollProps, ManimAnimationProps } from "./ManimScroll";
|
|
3
|
-
export { useManimScroll } from "./hooks";
|
|
4
|
-
export type { UseManimScrollOptions, UseManimScrollResult } from "./hooks";
|
|
3
|
+
export { useManimScroll, useNativeAnimation } from "./hooks";
|
|
4
|
+
export type { UseManimScrollOptions, UseManimScrollResult, UseNativeAnimationOptions, UseNativeAnimationResult, } from "./hooks";
|
|
5
5
|
export { computePropsHash, extractChildrenText } from "./hash";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mihirsarya/manim-scroll-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
15
|
+
"@mihirsarya/manim-scroll-runtime": "0.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/react": "^18.2.61",
|