@mihirsarya/manim-scroll-next 0.1.5 → 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 ADDED
@@ -0,0 +1,265 @@
1
+ # @mihirsarya/manim-scroll-next
2
+
3
+ Next.js plugin for build-time Manim scroll animation rendering. Automatically scans your source files, extracts `<ManimScroll>` components, and renders animations with smart caching.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mihirsarya/manim-scroll-next
9
+ ```
10
+
11
+ Or use the unified package (recommended):
12
+
13
+ ```bash
14
+ npm install @mihirsarya/manim-scroll
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - Next.js 13+
20
+ - Python 3.8+ with [Manim](https://www.manim.community/) installed
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Configure Next.js
25
+
26
+ ```js
27
+ // next.config.js
28
+ const { withManimScroll } = require("@mihirsarya/manim-scroll-next");
29
+ // Or: const { withManimScroll } = require("@mihirsarya/manim-scroll/next");
30
+
31
+ module.exports = withManimScroll({
32
+ manimScroll: {
33
+ pythonPath: "python3",
34
+ quality: "h",
35
+ },
36
+ });
37
+ ```
38
+
39
+ ### 2. Use ManimScroll Components
40
+
41
+ ```tsx
42
+ // app/page.tsx
43
+ import { ManimScroll } from "@mihirsarya/manim-scroll-react";
44
+ // Or: import { ManimScroll } from "@mihirsarya/manim-scroll";
45
+
46
+ export default function Home() {
47
+ return (
48
+ <ManimScroll
49
+ scene="TextScene"
50
+ fontSize={72}
51
+ color="#ffffff"
52
+ >
53
+ Welcome to my site
54
+ </ManimScroll>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ### 3. Build
60
+
61
+ ```bash
62
+ next build
63
+ ```
64
+
65
+ The plugin:
66
+ 1. Scans your source files for `<ManimScroll>` components
67
+ 2. Extracts props and children text
68
+ 3. Computes a hash for each unique animation
69
+ 4. Renders only new/changed animations (cached by hash)
70
+ 5. Outputs assets to `public/manim-assets/`
71
+
72
+ ## Configuration
73
+
74
+ ### Combined Config (Recommended)
75
+
76
+ ```js
77
+ module.exports = withManimScroll({
78
+ // Next.js config
79
+ reactStrictMode: true,
80
+
81
+ // Manim Scroll config
82
+ manimScroll: {
83
+ pythonPath: "python3",
84
+ quality: "h",
85
+ fps: 30,
86
+ resolution: "1920x1080",
87
+ verbose: true,
88
+ },
89
+ });
90
+ ```
91
+
92
+ ### Separate Configs
93
+
94
+ ```js
95
+ const nextConfig = {
96
+ reactStrictMode: true,
97
+ };
98
+
99
+ module.exports = withManimScroll(nextConfig, {
100
+ pythonPath: "python3",
101
+ quality: "h",
102
+ });
103
+ ```
104
+
105
+ ## Options
106
+
107
+ | Option | Type | Default | Description |
108
+ |--------|------|---------|-------------|
109
+ | `pythonPath` | `string` | `"python3"` | Path to Python executable |
110
+ | `cliPath` | `string` | Built-in | Path to render CLI script |
111
+ | `templatesDir` | `string` | Built-in | Path to scene templates |
112
+ | `quality` | `string` | `"h"` | Manim quality preset (`l`, `m`, `h`, `k`) |
113
+ | `fps` | `number` | `30` | Frames per second |
114
+ | `resolution` | `string` | `"1920x1080"` | Output resolution (`WIDTHxHEIGHT`) |
115
+ | `format` | `string` | `"both"` | Output format (`frames`, `video`, `both`) |
116
+ | `concurrency` | `number` | CPU count - 1 | Max parallel renders |
117
+ | `include` | `string[]` | `["**/*.tsx", "**/*.jsx"]` | Glob patterns to scan |
118
+ | `exclude` | `string[]` | `["node_modules/**", ".next/**"]` | Glob patterns to exclude |
119
+ | `cleanOrphans` | `boolean` | `true` | Remove unused cached assets |
120
+ | `verbose` | `boolean` | `false` | Enable verbose logging |
121
+
122
+ ## Exports
123
+
124
+ ### Main Export
125
+
126
+ - **`withManimScroll(config, manimConfig?)`** - Next.js config wrapper
127
+
128
+ ### Types
129
+
130
+ - `ManimScrollConfig` - Plugin configuration type
131
+ - `NextConfigWithManimScroll` - Extended Next.js config type
132
+
133
+ ### Utilities (Advanced)
134
+
135
+ ```ts
136
+ import {
137
+ extractAnimations,
138
+ renderAnimations,
139
+ computePropsHash,
140
+ isCached,
141
+ getCacheEntry,
142
+ getAnimationsToRender,
143
+ writeCacheManifest,
144
+ readCacheManifest,
145
+ cleanOrphanedCache,
146
+ processManimScroll,
147
+ } from "@mihirsarya/manim-scroll-next";
148
+ ```
149
+
150
+ ## How It Works
151
+
152
+ ### 1. Extraction
153
+
154
+ The plugin uses Babel to parse your source files and extract `<ManimScroll>` component usages:
155
+
156
+ ```tsx
157
+ <ManimScroll scene="TextScene" fontSize={72} color="#fff">
158
+ Hello World
159
+ </ManimScroll>
160
+ ```
161
+
162
+ Becomes:
163
+
164
+ ```ts
165
+ {
166
+ id: "app/page.tsx:ManimScroll:1",
167
+ scene: "TextScene",
168
+ props: { text: "Hello World", fontSize: 72, color: "#fff" }
169
+ }
170
+ ```
171
+
172
+ ### 2. Caching
173
+
174
+ Each animation is hashed based on `scene + props`. The cache manifest at `public/manim-assets/cache-manifest.json` maps hashes to rendered asset directories:
175
+
176
+ ```json
177
+ {
178
+ "version": 1,
179
+ "animations": {
180
+ "abc123...": "/manim-assets/abc123.../manifest.json"
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### 3. Rendering
186
+
187
+ New animations are rendered using the bundled Python CLI:
188
+
189
+ ```bash
190
+ python render/cli.py \
191
+ --scene-file render/templates/text_scene.py \
192
+ --scene-name TextScene \
193
+ --props '{"text": "Hello World", "fontSize": 72}' \
194
+ --output-dir public/manim-assets/abc123...
195
+ ```
196
+
197
+ ### 4. Asset Structure
198
+
199
+ ```
200
+ public/manim-assets/
201
+ ├── cache-manifest.json
202
+ ├── abc123.../
203
+ │ ├── manifest.json
204
+ │ ├── media/
205
+ │ │ ├── videos/...
206
+ │ │ └── images/...
207
+ └── def456.../
208
+ └── ...
209
+ ```
210
+
211
+ ## Development Mode
212
+
213
+ In dev mode (`next dev`), the plugin:
214
+ - Creates an empty cache manifest immediately to prevent 404s
215
+ - Processes animations asynchronously in the background
216
+ - Retries manifest resolution if animations are still rendering
217
+
218
+ ## Custom Build Scripts
219
+
220
+ For advanced workflows, use the exported utilities:
221
+
222
+ ```ts
223
+ import { extractAnimations, renderAnimations } from "@mihirsarya/manim-scroll-next";
224
+
225
+ async function customBuild() {
226
+ const animations = await extractAnimations({
227
+ rootDir: process.cwd(),
228
+ include: ["src/**/*.tsx"],
229
+ });
230
+
231
+ const results = await renderAnimations(animations, "./public", {
232
+ pythonPath: "python3",
233
+ quality: "h",
234
+ });
235
+
236
+ console.log(`Rendered ${results.filter(r => r.success).length} animations`);
237
+ }
238
+ ```
239
+
240
+ ## Troubleshooting
241
+
242
+ ### Manim not found
243
+
244
+ Ensure Manim is installed and accessible:
245
+
246
+ ```bash
247
+ python3 -c "import manim; print(manim.__version__)"
248
+ ```
249
+
250
+ ### Animations not updating
251
+
252
+ The cache is based on props hash. To force re-render:
253
+
254
+ 1. Delete `public/manim-assets/`
255
+ 2. Or change a prop value
256
+
257
+ ### Slow builds
258
+
259
+ - Reduce `quality` (use `"l"` or `"m"` for development)
260
+ - Increase `concurrency` if you have more CPU cores
261
+ - Use `format: "frames"` to skip video encoding
262
+
263
+ ## License
264
+
265
+ MIT
package/dist/extractor.js CHANGED
@@ -189,8 +189,25 @@ function isManimScrollComponent(node) {
189
189
  }
190
190
  return false;
191
191
  }
192
+ /**
193
+ * Props that should NOT be included in the animation hash.
194
+ * These are display/scroll-related props, not animation-specific props.
195
+ * This list must stay in sync with what ManimScroll.tsx excludes from animationProps.
196
+ */
197
+ const EXCLUDED_PROPS = new Set([
198
+ "manifestUrl",
199
+ "mode",
200
+ "scrollRange",
201
+ "onReady",
202
+ "onProgress",
203
+ "canvas",
204
+ "className",
205
+ "style",
206
+ "children", // children is handled separately as "text"
207
+ ]);
192
208
  /**
193
209
  * Extract ManimScroll component data from a JSX element.
210
+ * Returns null if the component uses native mode (no pre-rendering needed).
194
211
  */
195
212
  function extractManimScroll(jsxElement, filePath) {
196
213
  var _a, _b;
@@ -200,6 +217,7 @@ function extractManimScroll(jsxElement, filePath) {
200
217
  }
201
218
  const props = {};
202
219
  let scene = "TextScene";
220
+ let mode;
203
221
  // Extract attributes
204
222
  for (const attr of openingElement.attributes) {
205
223
  if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
@@ -208,8 +226,11 @@ function extractManimScroll(jsxElement, filePath) {
208
226
  if (name === "scene" && typeof value === "string") {
209
227
  scene = value;
210
228
  }
211
- else if (name === "manifestUrl") {
212
- // Skip manifestUrl - this is for explicit mode
229
+ else if (name === "mode" && typeof value === "string") {
230
+ mode = value;
231
+ }
232
+ else if (EXCLUDED_PROPS.has(name)) {
233
+ // Skip display/scroll-related props - only include animation-specific props
213
234
  continue;
214
235
  }
215
236
  else if (value !== undefined) {
@@ -217,6 +238,10 @@ function extractManimScroll(jsxElement, filePath) {
217
238
  }
218
239
  }
219
240
  }
241
+ // Skip native mode components - they render in the browser without pre-rendering
242
+ if (mode === "native") {
243
+ return null;
244
+ }
220
245
  // Extract children as text prop
221
246
  const childrenText = extractChildrenText(jsxElement.children);
222
247
  if (childrenText) {
@@ -237,6 +237,38 @@ vitest_1.vi.mock("glob", () => ({
237
237
  (0, vitest_1.expect)(result).toHaveLength(1);
238
238
  (0, vitest_1.expect)(result[0].props.data).toEqual({ "123": "value" });
239
239
  });
240
+ (0, vitest_1.it)("should exclude display/scroll-related props from animation props", () => {
241
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
242
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue(`
243
+ <ManimScroll
244
+ fontSize={72}
245
+ color="#ffffff"
246
+ scrollRange="viewport"
247
+ style={{ width: "100%", height: "100%" }}
248
+ className="animation-container"
249
+ mode="frames"
250
+ onReady={() => {}}
251
+ onProgress={(p) => console.log(p)}
252
+ canvas={{ width: 1920, height: 1080 }}
253
+ >
254
+ Hello World
255
+ </ManimScroll>
256
+ `);
257
+ const result = (0, extractor_1.extractAnimationsFromFile)("/app/page.tsx");
258
+ (0, vitest_1.expect)(result).toHaveLength(1);
259
+ // Animation props should be included
260
+ (0, vitest_1.expect)(result[0].props.fontSize).toBe(72);
261
+ (0, vitest_1.expect)(result[0].props.color).toBe("#ffffff");
262
+ (0, vitest_1.expect)(result[0].props.text).toBe("Hello World");
263
+ // Display/scroll props should be excluded
264
+ (0, vitest_1.expect)(result[0].props.scrollRange).toBeUndefined();
265
+ (0, vitest_1.expect)(result[0].props.style).toBeUndefined();
266
+ (0, vitest_1.expect)(result[0].props.className).toBeUndefined();
267
+ (0, vitest_1.expect)(result[0].props.mode).toBeUndefined();
268
+ (0, vitest_1.expect)(result[0].props.onReady).toBeUndefined();
269
+ (0, vitest_1.expect)(result[0].props.onProgress).toBeUndefined();
270
+ (0, vitest_1.expect)(result[0].props.canvas).toBeUndefined();
271
+ });
240
272
  });
241
273
  (0, vitest_1.describe)("extractChildrenText (via extractor)", () => {
242
274
  (0, vitest_1.beforeEach)(() => {
package/dist/index.d.ts CHANGED
@@ -35,9 +35,11 @@ declare function processManimScroll(projectDir: string, config: ManimScrollConfi
35
35
  /**
36
36
  * Wrap a Next.js config with ManimScroll build-time processing.
37
37
  *
38
+ * Supports two calling patterns:
39
+ *
38
40
  * @example
39
41
  * ```js
40
- * // next.config.js
42
+ * // Pattern 1: Combined config (recommended)
41
43
  * const { withManimScroll } = require("@mihirsarya/manim-scroll-next");
42
44
  *
43
45
  * module.exports = withManimScroll({
@@ -47,8 +49,17 @@ declare function processManimScroll(projectDir: string, config: ManimScrollConfi
47
49
  * },
48
50
  * });
49
51
  * ```
52
+ *
53
+ * @example
54
+ * ```js
55
+ * // Pattern 2: Separate configs
56
+ * module.exports = withManimScroll(nextConfig, {
57
+ * pythonPath: "python3",
58
+ * quality: "h",
59
+ * });
60
+ * ```
50
61
  */
51
- export declare function withManimScroll(nextConfig?: NextConfigWithManimScroll): NextConfig;
62
+ export declare function withManimScroll(nextConfig?: NextConfigWithManimScroll, manimScrollConfig?: ManimScrollConfig): NextConfig;
52
63
  export { extractAnimations, type ExtractedAnimation } from "./extractor";
53
64
  export { computePropsHash, isCached, getCacheEntry, getAnimationsToRender, writeCacheManifest, readCacheManifest, cleanOrphanedCache, } from "./cache";
54
65
  export { renderAnimations, type RenderResult, type RenderOptions } from "./renderer";
package/dist/index.js CHANGED
@@ -36,10 +36,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.renderAnimations = exports.cleanOrphanedCache = exports.readCacheManifest = exports.writeCacheManifest = exports.getAnimationsToRender = exports.getCacheEntry = exports.isCached = exports.computePropsHash = exports.extractAnimations = void 0;
37
37
  exports.withManimScroll = withManimScroll;
38
38
  exports.processManimScroll = processManimScroll;
39
+ const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
40
41
  const extractor_1 = require("./extractor");
41
42
  const cache_1 = require("./cache");
42
43
  const renderer_1 = require("./renderer");
44
+ /**
45
+ * Ensure a cache manifest file exists (preserving existing entries).
46
+ * This is called synchronously so the file exists before the page loads.
47
+ */
48
+ function ensureCacheManifestExists(publicDir) {
49
+ const manifestPath = path.join(publicDir, "manim-assets", "cache-manifest.json");
50
+ // If manifest already exists, leave it alone
51
+ if (fs.existsSync(manifestPath)) {
52
+ return;
53
+ }
54
+ // Create the directory and an empty manifest
55
+ (0, cache_1.ensureAssetDir)(publicDir);
56
+ fs.writeFileSync(manifestPath, JSON.stringify({ version: 1, animations: {} }, null, 2));
57
+ }
43
58
  let hasProcessed = false;
44
59
  /**
45
60
  * Process ManimScroll components: extract, cache, and render.
@@ -74,6 +89,11 @@ async function processManimScroll(projectDir, config) {
74
89
  console.log(` - ${animation.id} (hash: ${hash})`);
75
90
  }
76
91
  }
92
+ // Write the cache manifest IMMEDIATELY after extraction, before rendering.
93
+ // This ensures the manifest is available when the page loads, even if
94
+ // rendering is still in progress. The runtime will gracefully handle
95
+ // missing animation files.
96
+ (0, cache_1.writeCacheManifest)(animations, publicDir);
77
97
  // Determine which need rendering
78
98
  const { cached, toRender } = (0, cache_1.getAnimationsToRender)(animations, publicDir);
79
99
  if (verbose && cached.length > 0) {
@@ -101,8 +121,6 @@ async function processManimScroll(projectDir, config) {
101
121
  else if (verbose) {
102
122
  console.log("[manim-scroll] All animations are cached, skipping render.");
103
123
  }
104
- // Write the cache manifest for runtime lookup
105
- (0, cache_1.writeCacheManifest)(animations, publicDir);
106
124
  // Clean up orphaned cache entries
107
125
  if (config.cleanOrphans !== false) {
108
126
  (0, cache_1.cleanOrphanedCache)(animations, publicDir);
@@ -112,9 +130,11 @@ async function processManimScroll(projectDir, config) {
112
130
  /**
113
131
  * Wrap a Next.js config with ManimScroll build-time processing.
114
132
  *
133
+ * Supports two calling patterns:
134
+ *
115
135
  * @example
116
136
  * ```js
117
- * // next.config.js
137
+ * // Pattern 1: Combined config (recommended)
118
138
  * const { withManimScroll } = require("@mihirsarya/manim-scroll-next");
119
139
  *
120
140
  * module.exports = withManimScroll({
@@ -124,10 +144,22 @@ async function processManimScroll(projectDir, config) {
124
144
  * },
125
145
  * });
126
146
  * ```
147
+ *
148
+ * @example
149
+ * ```js
150
+ * // Pattern 2: Separate configs
151
+ * module.exports = withManimScroll(nextConfig, {
152
+ * pythonPath: "python3",
153
+ * quality: "h",
154
+ * });
155
+ * ```
127
156
  */
128
- function withManimScroll(nextConfig = {}) {
157
+ function withManimScroll(nextConfig = {}, manimScrollConfig) {
129
158
  var _a;
130
- const manimConfig = (_a = nextConfig.manimScroll) !== null && _a !== void 0 ? _a : {};
159
+ // Support both calling patterns:
160
+ // 1. withManimScroll({ manimScroll: {...} })
161
+ // 2. withManimScroll(nextConfig, { pythonPath: ... })
162
+ const manimConfig = (_a = manimScrollConfig !== null && manimScrollConfig !== void 0 ? manimScrollConfig : nextConfig.manimScroll) !== null && _a !== void 0 ? _a : {};
131
163
  // Remove manimScroll from the config passed to Next.js
132
164
  const { manimScroll: _, ...restConfig } = nextConfig;
133
165
  return {
@@ -153,7 +185,13 @@ function withManimScroll(nextConfig = {}) {
153
185
  }
154
186
  // Run in dev mode on first build
155
187
  if (context.dev && context.isServer) {
156
- processManimScroll(context.dir, manimConfig).catch((error) => {
188
+ const projectDir = context.dir;
189
+ const publicDir = path.join(projectDir, "public");
190
+ // Ensure a cache manifest exists SYNCHRONOUSLY before the page loads.
191
+ // This prevents 404 errors while async processing is running.
192
+ // The manifest will be updated with actual animations once extraction completes.
193
+ ensureCacheManifestExists(publicDir);
194
+ processManimScroll(projectDir, manimConfig).catch((error) => {
157
195
  console.error("[manim-scroll] Error during dev processing:", error);
158
196
  });
159
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mihirsarya/manim-scroll-next",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Next.js plugin for build-time Manim scroll animation rendering.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,11 +8,6 @@
8
8
  "dist",
9
9
  "render"
10
10
  ],
11
- "scripts": {
12
- "prebuild": "rm -rf render && cp -r ../render .",
13
- "build": "tsc -p tsconfig.json",
14
- "prepublishOnly": "rm -rf render && cp -r ../render . && npm run build"
15
- },
16
11
  "peerDependencies": {
17
12
  "next": ">=13.0.0"
18
13
  },
@@ -26,5 +21,9 @@
26
21
  "@types/babel__traverse": "^7.20.5",
27
22
  "@types/node": "^20.11.0",
28
23
  "typescript": "^5.4.5"
24
+ },
25
+ "scripts": {
26
+ "prebuild": "rm -rf render && cp -r ../render .",
27
+ "build": "tsc -p tsconfig.json"
29
28
  }
30
- }
29
+ }
package/render/cli.py CHANGED
@@ -6,7 +6,7 @@ import json
6
6
  import os
7
7
  import subprocess
8
8
  import sys
9
- from dataclasses import dataclass, asdict
9
+ from dataclasses import dataclass, asdict, field
10
10
  from pathlib import Path
11
11
  from typing import List, Optional
12
12
 
@@ -19,6 +19,9 @@ class RenderManifest:
19
19
  height: int
20
20
  frames: List[str]
21
21
  video: Optional[str]
22
+ transparent: bool = False
23
+ inline: bool = False
24
+ aspectRatio: Optional[float] = None
22
25
 
23
26
 
24
27
  def _parse_resolution(value: str) -> tuple[int, int]:
@@ -51,6 +54,16 @@ def _run_manim(cmd: List[str], env: dict[str, str]) -> None:
51
54
  raise RuntimeError("Manim render failed. Check the command and logs.")
52
55
 
53
56
 
57
+ def _load_props_file(props_path: Optional[str]) -> dict:
58
+ """Load props from a JSON file."""
59
+ if not props_path:
60
+ return {}
61
+ path = Path(props_path)
62
+ if not path.exists():
63
+ return {}
64
+ return json.loads(path.read_text())
65
+
66
+
54
67
  def main() -> int:
55
68
  parser = argparse.ArgumentParser(description="Render Manim scenes for scroll playback.")
56
69
  parser.add_argument("--scene-file", required=True, help="Path to Manim scene file.")
@@ -65,11 +78,20 @@ def main() -> int:
65
78
  "--props",
66
79
  help="Path to JSON props for the scene (exposed as MANIM_SCROLL_PROPS).",
67
80
  )
81
+ parser.add_argument(
82
+ "--transparent",
83
+ action="store_true",
84
+ help="Render with transparent background (for inline mode).",
85
+ )
68
86
 
69
87
  args = parser.parse_args()
70
88
  output_dir = Path(args.output_dir).resolve()
71
89
  output_dir.mkdir(parents=True, exist_ok=True)
72
90
 
91
+ # Load props to check for inline mode
92
+ props = _load_props_file(args.props)
93
+ inline = props.get("inline", False) or args.transparent
94
+
73
95
  width, height = args.resolution
74
96
  base_cmd = [
75
97
  "manim",
@@ -84,18 +106,38 @@ def main() -> int:
84
106
  args.scene_name,
85
107
  ]
86
108
 
109
+ # Add transparent flag for inline mode
110
+ if inline or args.transparent:
111
+ base_cmd.insert(1, "--transparent")
112
+
87
113
  env = os.environ.copy()
88
114
  if args.props:
89
115
  props_path = Path(args.props).resolve()
90
116
  env["MANIM_SCROLL_PROPS"] = str(props_path)
91
117
 
118
+ # Set up bounds output file for inline mode
119
+ bounds_path = output_dir / "bounds.json"
120
+ env["MANIM_SCROLL_BOUNDS_OUT"] = str(bounds_path)
121
+
92
122
  if args.format in ("frames", "both"):
93
123
  _run_manim(base_cmd + ["--write_all", "--format", "png"], env)
94
124
 
95
125
  if args.format in ("video", "both"):
96
- _run_manim(base_cmd + ["--format", args.video_format], env)
126
+ # For transparent video, use webm which supports alpha
127
+ video_format = "webm" if (inline or args.transparent) else args.video_format
128
+ _run_manim(base_cmd + ["--format", video_format], env)
97
129
 
98
130
  frames, video = _collect_assets(output_dir)
131
+
132
+ # For inline mode, try to read bounds info to get aspect ratio
133
+ aspect_ratio = None
134
+ if bounds_path.exists():
135
+ try:
136
+ bounds_data = json.loads(bounds_path.read_text())
137
+ aspect_ratio = bounds_data.get("aspectRatio")
138
+ except Exception:
139
+ pass
140
+
99
141
  manifest = RenderManifest(
100
142
  scene=args.scene_name,
101
143
  fps=args.fps,
@@ -103,6 +145,9 @@ def main() -> int:
103
145
  height=height,
104
146
  frames=frames,
105
147
  video=video,
148
+ transparent=inline or args.transparent,
149
+ inline=inline,
150
+ aspectRatio=aspect_ratio,
106
151
  )
107
152
  manifest_path = output_dir / "manifest.json"
108
153
  manifest_path.write_text(json.dumps(asdict(manifest), indent=2))
@@ -4,7 +4,7 @@ import json
4
4
  import os
5
5
  from pathlib import Path
6
6
 
7
- from manim import ORIGIN, Scene, Text, Write
7
+ from manim import ORIGIN, Scene, Text, Write, config
8
8
 
9
9
 
10
10
  def _load_props() -> dict:
@@ -18,6 +18,21 @@ def _load_props() -> dict:
18
18
  return json.load(handle)
19
19
 
20
20
 
21
+ def _write_bounds_info(width: float, height: float, aspect_ratio: float, props: dict) -> None:
22
+ """Write text bounds info to a file for the manifest generator."""
23
+ bounds_path = os.environ.get("MANIM_SCROLL_BOUNDS_OUT")
24
+ if not bounds_path:
25
+ return
26
+ bounds = {
27
+ "textWidth": width,
28
+ "textHeight": height,
29
+ "aspectRatio": aspect_ratio,
30
+ "inline": props.get("inline", False),
31
+ "padding": props.get("padding", 0),
32
+ }
33
+ Path(bounds_path).write_text(json.dumps(bounds))
34
+
35
+
21
36
  class TextScene(Scene):
22
37
  def construct(self) -> None:
23
38
  props = _load_props()
@@ -25,12 +40,35 @@ class TextScene(Scene):
25
40
  font_size = props.get("fontSize", props.get("font_size", 64))
26
41
  color = props.get("color", "#FFFFFF")
27
42
  font = props.get("font")
43
+ inline = props.get("inline", False)
44
+ padding = props.get("padding", 0.1) # Small padding in Manim units
28
45
 
29
46
  if font:
30
47
  text_mob = Text(text_value, font_size=font_size, color=color, font=font)
31
48
  else:
32
49
  text_mob = Text(text_value, font_size=font_size, color=color)
33
50
 
51
+ if inline:
52
+ # For inline mode, adjust the camera frame to fit the text tightly
53
+ # This makes the text fill the entire frame
54
+ text_width = text_mob.width
55
+ text_height = text_mob.height
56
+
57
+ # Add minimal padding
58
+ padded_width = text_width + padding * 2
59
+ padded_height = text_height + padding * 2
60
+
61
+ # Calculate aspect ratio
62
+ aspect_ratio = padded_width / padded_height if padded_height > 0 else 1
63
+
64
+ # Adjust the Manim frame to match text bounds
65
+ # This sets the "virtual" frame size in Manim units
66
+ config.frame_width = padded_width
67
+ config.frame_height = padded_height
68
+
69
+ # Write bounds info for the manifest
70
+ _write_bounds_info(text_width, text_height, aspect_ratio, props)
71
+
34
72
  text_mob.move_to(ORIGIN)
35
73
  self.play(Write(text_mob))
36
74
  self.wait(0.5)