@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 +12 -0
- package/package.json +9 -6
- package/render/cli.py +115 -0
- package/render/pyproject.toml +14 -0
- package/render/templates/text_scene.py +36 -0
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.
|
|
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)
|