@mihirsarya/manim-scroll-next 0.1.1 → 0.1.3

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/renderer.js CHANGED
@@ -55,10 +55,16 @@ function findCliPath(providedPath) {
55
55
  return providedPath;
56
56
  }
57
57
  // Try to find the CLI relative to the package
58
+ // When installed, __dirname is next/dist/, render is at next/render/
58
59
  const candidates = [
60
+ // When running from dist/, render/ is a sibling directory
61
+ path.resolve(__dirname, "../render/cli.py"),
62
+ // Legacy paths for development
59
63
  path.resolve(__dirname, "../../render/cli.py"),
60
64
  path.resolve(__dirname, "../../../render/cli.py"),
65
+ // When running from project root
61
66
  path.resolve(process.cwd(), "render/cli.py"),
67
+ // When installed in node_modules
62
68
  path.resolve(process.cwd(), "node_modules/@mihirsarya/manim-scroll-next/render/cli.py"),
63
69
  ];
64
70
  for (const candidate of candidates) {
@@ -75,10 +81,16 @@ function findTemplatesDir(providedPath) {
75
81
  if (providedPath) {
76
82
  return providedPath;
77
83
  }
84
+ // When installed, __dirname is next/dist/, templates are at next/render/templates/
78
85
  const candidates = [
86
+ // When running from dist/, render/ is a sibling directory
87
+ path.resolve(__dirname, "../render/templates"),
88
+ // Legacy paths for development
79
89
  path.resolve(__dirname, "../../render/templates"),
80
90
  path.resolve(__dirname, "../../../render/templates"),
91
+ // When running from project root
81
92
  path.resolve(process.cwd(), "render/templates"),
93
+ // When installed in node_modules
82
94
  path.resolve(process.cwd(), "node_modules/@mihirsarya/manim-scroll-next/render/templates"),
83
95
  ];
84
96
  for (const candidate of candidates) {
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@mihirsarya/manim-scroll-next",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
7
7
  "files": [
8
- "dist"
8
+ "dist",
9
+ "render"
9
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
+ },
10
16
  "peerDependencies": {
11
17
  "next": ">=13.0.0"
12
18
  },
@@ -20,8 +26,5 @@
20
26
  "@types/babel__traverse": "^7.20.5",
21
27
  "@types/node": "^20.11.0",
22
28
  "typescript": "^5.4.5"
23
- },
24
- "scripts": {
25
- "build": "tsc -p tsconfig.json"
26
29
  }
27
- }
30
+ }
package/render/cli.py ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from dataclasses import dataclass, asdict
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+
13
+
14
+ @dataclass
15
+ class RenderManifest:
16
+ scene: str
17
+ fps: int
18
+ width: int
19
+ height: int
20
+ frames: List[str]
21
+ video: Optional[str]
22
+
23
+
24
+ def _parse_resolution(value: str) -> tuple[int, int]:
25
+ if "x" in value:
26
+ w, h = value.split("x", 1)
27
+ elif "," in value:
28
+ w, h = value.split(",", 1)
29
+ else:
30
+ raise argparse.ArgumentTypeError("Resolution must be WxH or W,H")
31
+ return int(w), int(h)
32
+
33
+
34
+ def _find_latest(files: List[Path]) -> Optional[Path]:
35
+ if not files:
36
+ return None
37
+ return max(files, key=lambda p: p.stat().st_mtime)
38
+
39
+
40
+ def _collect_assets(media_dir: Path) -> tuple[List[str], Optional[str]]:
41
+ frames = sorted(
42
+ [p.relative_to(media_dir).as_posix() for p in media_dir.rglob("*.png")]
43
+ )
44
+ video = _find_latest([*media_dir.rglob("*.mp4"), *media_dir.rglob("*.webm")])
45
+ return frames, video.relative_to(media_dir).as_posix() if video else None
46
+
47
+
48
+ def _run_manim(cmd: List[str], env: dict[str, str]) -> None:
49
+ result = subprocess.run(cmd, check=False, env=env)
50
+ if result.returncode != 0:
51
+ raise RuntimeError("Manim render failed. Check the command and logs.")
52
+
53
+
54
+ def main() -> int:
55
+ parser = argparse.ArgumentParser(description="Render Manim scenes for scroll playback.")
56
+ parser.add_argument("--scene-file", required=True, help="Path to Manim scene file.")
57
+ parser.add_argument("--scene-name", required=True, help="Scene class name.")
58
+ parser.add_argument("--output-dir", required=True, help="Directory for render outputs.")
59
+ parser.add_argument("--format", choices=["frames", "video", "both"], default="both")
60
+ parser.add_argument("--fps", type=int, default=30)
61
+ parser.add_argument("--resolution", type=_parse_resolution, default=(1920, 1080))
62
+ parser.add_argument("--quality", default="k", help="Manim quality preset (l, m, h, k).")
63
+ parser.add_argument("--video-format", choices=["mp4", "webm"], default="mp4")
64
+ parser.add_argument(
65
+ "--props",
66
+ help="Path to JSON props for the scene (exposed as MANIM_SCROLL_PROPS).",
67
+ )
68
+
69
+ args = parser.parse_args()
70
+ output_dir = Path(args.output_dir).resolve()
71
+ output_dir.mkdir(parents=True, exist_ok=True)
72
+
73
+ width, height = args.resolution
74
+ base_cmd = [
75
+ "manim",
76
+ f"-q{args.quality}",
77
+ "--media_dir",
78
+ str(output_dir),
79
+ "--fps",
80
+ str(args.fps),
81
+ "--resolution",
82
+ f"{width},{height}",
83
+ args.scene_file,
84
+ args.scene_name,
85
+ ]
86
+
87
+ env = os.environ.copy()
88
+ if args.props:
89
+ props_path = Path(args.props).resolve()
90
+ env["MANIM_SCROLL_PROPS"] = str(props_path)
91
+
92
+ if args.format in ("frames", "both"):
93
+ _run_manim(base_cmd + ["--write_all_frames", "--format", "png"], env)
94
+
95
+ if args.format in ("video", "both"):
96
+ _run_manim(base_cmd + ["--format", args.video_format], env)
97
+
98
+ frames, video = _collect_assets(output_dir)
99
+ manifest = RenderManifest(
100
+ scene=args.scene_name,
101
+ fps=args.fps,
102
+ width=width,
103
+ height=height,
104
+ frames=frames,
105
+ video=video,
106
+ )
107
+ manifest_path = output_dir / "manifest.json"
108
+ manifest_path.write_text(json.dumps(asdict(manifest), indent=2))
109
+
110
+ print(f"Wrote manifest to {manifest_path}")
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "manim-scroll-render"
3
+ version = "0.1.0"
4
+ description = "Render pipeline for Manim scroll animations."
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "manim>=0.18.0"
8
+ ]
9
+
10
+ [project.scripts]
11
+ manim-scroll-render = "cli:main"
12
+
13
+ [tool.setuptools]
14
+ py-modules = ["cli"]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from manim import ORIGIN, Scene, Text, Write
8
+
9
+
10
+ def _load_props() -> dict:
11
+ props_path = os.environ.get("MANIM_SCROLL_PROPS")
12
+ if not props_path:
13
+ return {}
14
+ path = Path(props_path)
15
+ if not path.exists():
16
+ return {}
17
+ with path.open("r", encoding="utf-8") as handle:
18
+ return json.load(handle)
19
+
20
+
21
+ class TextScene(Scene):
22
+ def construct(self) -> None:
23
+ props = _load_props()
24
+ text_value = props.get("text", "Hello Manim")
25
+ font_size = props.get("fontSize", props.get("font_size", 64))
26
+ color = props.get("color", "#FFFFFF")
27
+ font = props.get("font")
28
+
29
+ if font:
30
+ text_mob = Text(text_value, font_size=font_size, color=color, font=font)
31
+ else:
32
+ text_mob = Text(text_value, font_size=font_size, color=color)
33
+
34
+ text_mob.move_to(ORIGIN)
35
+ self.play(Write(text_mob))
36
+ self.wait(0.5)