Skip to content

Commit ed1b2eb

Browse files
authored
Merge pull request #6 from jeertmans/manimgl
feat: add support for manimgl
2 parents c53e410 + cda304f commit ed1b2eb

File tree

6 files changed

+137
-27
lines changed

6 files changed

+137
-27
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ __pycache__/
1313
slides/
1414

1515
.manim-slides.json
16+
17+
videos/
18+
19+
images/

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides)
44
# Manim Slides
55

6-
Tool for live presentations extending [manim-community](https://www.manim.community/)'s capabilities. Currently, support for 3b1b's manim is not planned.
6+
Tool for live presentations using either [manim-community](https://www.manim.community/) or [manimgl](https://3b1b.github.io/manim/). `manim-slides` will automatically detect the one you are using!
77

8-
> **_NOTE:_** This project is a fork of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation). Since the project seemed to be inactive, I decided to create my own fork to deploy new features more rapidly.
8+
> **_NOTE:_** This project extends to work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
99
1010
## Install
1111

@@ -29,6 +29,7 @@ call `self.pause()` when you want to pause the playback and wait for an input to
2929
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
3030
```python
3131
from manim import *
32+
# or: from manimlib import *
3233
from manim_slides import Slide
3334

3435
class Example(Slide):
@@ -76,7 +77,7 @@ You can run the **configuration wizard** with:
7677
manim-slides wizard
7778
```
7879

79-
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REWIND` and `PLAY_PAUSE`.
80+
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REVERSE` `REWIND` and `PLAY_PAUSE`.
8081

8182
A default file can be created with:
8283
```
@@ -96,11 +97,15 @@ cd manim-slides
9697
Install `manim` and `manim-slides`:
9798
```
9899
pip install manim manim-slides
100+
# or
101+
pip install manimgl manim-slides
99102
```
100103

101104
Render the example scene:
102105
```
103-
manim -qh example.py
106+
manim -qh example.py Example
107+
# or
108+
manimgl --hd example.py Example
104109
```
105110

106111
Run the presentation
@@ -125,6 +130,7 @@ Here are a few things that I implemented (or that I'm planning to implement) on
125130
- [x] Config file path can be manually set
126131
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
127132
- [x] Handle 3D scenes out of the box
133+
- [x] Support for both `manim` and `manimgl` modules
128134
- [ ] Generate docs online
129135
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
130136

example.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from manim import *
1+
# If you want to use manimgl, uncomment change
2+
# manim to manimlib
3+
from manimlib import *
24

35
from manim_slides import Slide, ThreeDSlide
46

manim_slides/manim.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import sys
2+
from importlib.util import find_spec
3+
4+
MANIM_PACKAGE_NAME = "manim"
5+
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
6+
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
7+
8+
MANIMGL_PACKAGE_NAME = "manimlib"
9+
MANIMGL_AVAILABLE = find_spec(MANIMGL_PACKAGE_NAME) is not None
10+
MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
11+
12+
if MANIM_IMPORTED and MANIMGL_IMPORTED:
13+
from manim import logger
14+
15+
logger.warn(
16+
"Both manim and manimgl are installed, therefore `manim-slide` needs to need which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
17+
)
18+
MANIM = True
19+
MANIMGL = False
20+
elif MANIM_AVAILABLE and not MANIMGL_IMPORTED:
21+
MANIM = True
22+
MANIMGL = False
23+
elif MANIMGL_AVAILABLE:
24+
MANIM = False
25+
MANIMGL = True
26+
else:
27+
raise ImportError(
28+
"Either manim (community) or manimgl (3b1b) package must be installed"
29+
)
30+
31+
32+
FFMPEG_BIN = None
33+
34+
if MANIMGL:
35+
from manimlib import Scene, ThreeDScene, config
36+
from manimlib.constants import FFMPEG_BIN
37+
from manimlib.logger import log as logger
38+
39+
else:
40+
from manim import Scene, ThreeDScene, config, logger
41+
42+
try: # For manim<v0.16.0.post0
43+
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
44+
except ImportError:
45+
FFMPEG_BIN = config.ffmpeg_executable

manim_slides/present.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,25 +377,29 @@ def value_proc(value: str):
377377
indices = list(map(int, value.strip().replace(" ", "").split(",")))
378378

379379
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
380-
raise ValueError("Please only enter numbers displayed on the screen.")
380+
raise click.UsageError(
381+
"Please only enter numbers displayed on the screen."
382+
)
381383

382384
return [scene_choices[i] for i in indices]
383385

384386
if len(scene_choices) == 0:
385-
raise ValueError("No scenes were found, are you in the correct directory?")
387+
raise click.UsageError(
388+
"No scenes were found, are you in the correct directory?"
389+
)
386390

387391
while True:
388392
try:
389393
scenes = click.prompt("Choice(s)", value_proc=value_proc)
390394
break
391395
except ValueError as e:
392-
click.secho(e, fg="red")
396+
raise click.UsageError(e)
393397

394398
presentations = list()
395399
for scene in scenes:
396400
config_file = os.path.join(folder, f"{scene}.json")
397401
if not os.path.exists(config_file):
398-
raise Exception(
402+
raise click.UsageError(
399403
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
400404
)
401405
config = json.load(open(config_file))

manim_slides/slide.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@
44
import shutil
55
import subprocess
66

7-
from manim import Scene, ThreeDScene, config, logger
87
from tqdm import tqdm
98

10-
try: # For manim<v0.16.0.post0
11-
from manim.constants import FFMPEG_BIN as ffmpeg_executable
12-
except ImportError:
13-
ffmpeg_executable = config.ffmpeg_executable
14-
159
from .defaults import FOLDER_PATH
10+
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
1611

1712

1813
def reverse_video_path(src: str) -> str:
@@ -21,21 +16,62 @@ def reverse_video_path(src: str) -> str:
2116

2217

2318
def reverse_video_file(src: str, dst: str):
24-
command = [config.ffmpeg_executable, "-i", src, "-vf", "reverse", dst]
19+
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
2520
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2621
process.communicate()
2722

2823

2924
class Slide(Scene):
3025
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
26+
if MANIMGL:
27+
if not os.path.isdir("videos"):
28+
os.mkdir("videos")
29+
kwargs["file_writer_config"] = {
30+
"break_into_partial_movies": True,
31+
"output_directory": "",
32+
"write_to_movie": True,
33+
}
34+
35+
kwargs["preview"] = False
36+
3137
super().__init__(*args, **kwargs)
38+
3239
self.output_folder = output_folder
3340
self.slides = list()
3441
self.current_slide = 1
3542
self.current_animation = 0
3643
self.loop_start_animation = None
3744
self.pause_start_animation = 0
3845

46+
@property
47+
def partial_movie_files(self):
48+
if MANIMGL:
49+
from manimlib.utils.file_ops import get_sorted_integer_files
50+
51+
kwargs = {
52+
"remove_non_integer_files": True,
53+
"extension": self.file_writer.movie_file_extension,
54+
}
55+
return get_sorted_integer_files(
56+
self.file_writer.partial_movie_directory, **kwargs
57+
)
58+
else:
59+
return self.renderer.file_writer.partial_movie_files
60+
61+
@property
62+
def show_progress_bar(self):
63+
if MANIMGL:
64+
return getattr(super(Scene, self), "show_progress_bar", True)
65+
else:
66+
return config["progress_bar"] != "none"
67+
68+
@property
69+
def leave_progress_bar(self):
70+
if MANIMGL:
71+
return getattr(super(Scene, self), "leave_progress_bars", False)
72+
else:
73+
return config["progress_bar"] == "leave"
74+
3975
def play(self, *args, **kwargs):
4076
super().play(*args, **kwargs)
4177
self.current_animation += 1
@@ -72,14 +108,7 @@ def end_loop(self):
72108
self.loop_start_animation = None
73109
self.pause_start_animation = self.current_animation
74110

75-
def render(self, *args, **kwargs):
76-
# We need to disable the caching limit since we rely on intermidiate files
77-
max_files_cached = config["max_files_cached"]
78-
config["max_files_cached"] = float("inf")
79-
80-
super().render(*args, **kwargs)
81-
82-
config["max_files_cached"] = max_files_cached
111+
def save_slides(self, use_cache=True):
83112

84113
if not os.path.exists(self.output_folder):
85114
os.mkdir(self.output_folder)
@@ -95,16 +124,19 @@ def render(self, *args, **kwargs):
95124

96125
if not os.path.exists(scene_files_folder):
97126
os.mkdir(scene_files_folder)
127+
elif not use_cache:
128+
shutil.rmtree(scene_files_folder)
129+
os.mkdir(scene_files_folder)
98130
else:
99131
old_animation_files.update(os.listdir(scene_files_folder))
100132

101133
files = list()
102134
for src_file in tqdm(
103-
self.renderer.file_writer.partial_movie_files,
135+
self.partial_movie_files,
104136
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
105-
leave=config["progress_bar"] == "leave",
137+
leave=self.leave_progress_bar,
106138
ascii=True if platform.system() == "Windows" else None,
107-
disable=config["progress_bar"] == "none",
139+
disable=not self.show_progress_bar,
108140
):
109141
filename = os.path.basename(src_file)
110142
_hash, ext = os.path.splitext(filename)
@@ -140,6 +172,23 @@ def render(self, *args, **kwargs):
140172
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
141173
)
142174

175+
def run(self, *args, **kwargs):
176+
"""MANIMGL renderer"""
177+
super().run(*args, **kwargs)
178+
self.save_slides(use_cache=False)
179+
180+
def render(self, *args, **kwargs):
181+
"""MANIM render"""
182+
# We need to disable the caching limit since we rely on intermidiate files
183+
max_files_cached = config["max_files_cached"]
184+
config["max_files_cached"] = float("inf")
185+
186+
super().render(*args, **kwargs)
187+
188+
config["max_files_cached"] = max_files_cached
189+
190+
self.save_slides()
191+
143192

144193
class ThreeDSlide(Slide, ThreeDScene):
145194
pass

0 commit comments

Comments
 (0)