mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-19 19:46:49 +08:00
feat: add support for manimgl
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -13,3 +13,7 @@ __pycache__/
|
||||
slides/
|
||||
|
||||
.manim-slides.json
|
||||
|
||||
videos/
|
||||
|
||||
images/
|
||||
|
14
README.md
14
README.md
@ -3,9 +3,9 @@
|
||||

|
||||
# Manim Slides
|
||||
|
||||
Tool for live presentations using either [manim-community](https://www.manim.community/). Currently, support for 3b1b's manim is not planned.
|
||||
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!
|
||||
|
||||
> **_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.
|
||||
> **_NOTE:_** This project extends to work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
|
||||
|
||||
## Install
|
||||
|
||||
@ -29,6 +29,7 @@ call `self.pause()` when you want to pause the playback and wait for an input to
|
||||
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
|
||||
```python
|
||||
from manim import *
|
||||
# or: from manimlib import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
@ -76,7 +77,7 @@ You can run the **configuration wizard** with:
|
||||
manim-slides wizard
|
||||
```
|
||||
|
||||
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REWIND` and `PLAY_PAUSE`.
|
||||
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REVERSE` `REWIND` and `PLAY_PAUSE`.
|
||||
|
||||
A default file can be created with:
|
||||
```
|
||||
@ -96,11 +97,15 @@ cd manim-slides
|
||||
Install `manim` and `manim-slides`:
|
||||
```
|
||||
pip install manim manim-slides
|
||||
# or
|
||||
pip install manimgl manim-slides
|
||||
```
|
||||
|
||||
Render the example scene:
|
||||
```
|
||||
manim -qh example.py
|
||||
manim -qh example.py Example
|
||||
# or
|
||||
manimgl --hd example.py Example
|
||||
```
|
||||
|
||||
Run the presentation
|
||||
@ -125,6 +130,7 @@ Here are a few things that I implemented (or that I'm planning to implement) on
|
||||
- [x] Config file path can be manually set
|
||||
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
|
||||
- [x] Handle 3D scenes out of the box
|
||||
- [x] Support for both `manim` and `manimgl` modules
|
||||
- [ ] Generate docs online
|
||||
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
from manim import *
|
||||
# If you want to use manimgl, uncomment change
|
||||
# manim to manimlib
|
||||
from manimlib import *
|
||||
|
||||
from manim_slides import Slide, ThreeDSlide
|
||||
|
||||
|
40
manim_slides/manim.py
Normal file
40
manim_slides/manim.py
Normal file
@ -0,0 +1,40 @@
|
||||
from importlib.util import find_spec
|
||||
import sys
|
||||
|
||||
MANIM_PACKAGE_NAME = "manim"
|
||||
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
|
||||
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
|
||||
|
||||
MANIMGL_PACKAGE_NAME = "manimlib"
|
||||
MANIMGL_AVAILABLE = find_spec(MANIMGL_PACKAGE_NAME) is not None
|
||||
MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
|
||||
|
||||
if MANIM_IMPORTED and MANIMGL_IMPORTED:
|
||||
from manim import logger
|
||||
logger.warn("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")
|
||||
MANIM = True
|
||||
MANIMGL = False
|
||||
elif MANIM_AVAILABLE and not MANIMGL_IMPORTED:
|
||||
MANIM = True
|
||||
MANIMGL = False
|
||||
elif MANIMGL_AVAILABLE:
|
||||
MANIM = False
|
||||
MANIMGL = True
|
||||
else:
|
||||
raise ImportError("Either manim (community) or manimgl (3b1b) package must be installed")
|
||||
|
||||
|
||||
FFMPEG_BIN = None
|
||||
|
||||
if MANIMGL:
|
||||
from manimlib import Scene, ThreeDScene, config
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.logger import log as logger
|
||||
|
||||
else:
|
||||
from manim import Scene, ThreeDScene, config, logger
|
||||
|
||||
try: # For manim<v0.16.0.post0
|
||||
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
|
||||
except ImportError:
|
||||
FFMPEG_BIN = config.ffmpeg_executable
|
@ -377,25 +377,25 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
|
||||
indices = list(map(int, value.strip().replace(" ", "").split(",")))
|
||||
|
||||
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
|
||||
raise ValueError("Please only enter numbers displayed on the screen.")
|
||||
raise click.UsageError("Please only enter numbers displayed on the screen.")
|
||||
|
||||
return [scene_choices[i] for i in indices]
|
||||
|
||||
if len(scene_choices) == 0:
|
||||
raise ValueError("No scenes were found, are you in the correct directory?")
|
||||
raise click.UsageError("No scenes were found, are you in the correct directory?")
|
||||
|
||||
while True:
|
||||
try:
|
||||
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||
break
|
||||
except ValueError as e:
|
||||
click.secho(e, fg="red")
|
||||
raise click.UsageError(e)
|
||||
|
||||
presentations = list()
|
||||
for scene in scenes:
|
||||
config_file = os.path.join(folder, f"{scene}.json")
|
||||
if not os.path.exists(config_file):
|
||||
raise Exception(
|
||||
raise click.UsageError(
|
||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||
)
|
||||
config = json.load(open(config_file))
|
||||
|
@ -4,15 +4,10 @@ import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from manim import Scene, ThreeDScene, config, logger
|
||||
from tqdm import tqdm
|
||||
|
||||
try: # For manim<v0.16.0.post0
|
||||
from manim.constants import FFMPEG_BIN as ffmpeg_executable
|
||||
except ImportError:
|
||||
ffmpeg_executable = config.ffmpeg_executable
|
||||
|
||||
from .defaults import FOLDER_PATH
|
||||
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
||||
|
||||
|
||||
def reverse_video_path(src: str) -> str:
|
||||
@ -21,14 +16,26 @@ def reverse_video_path(src: str) -> str:
|
||||
|
||||
|
||||
def reverse_video_file(src: str, dst: str):
|
||||
command = [config.ffmpeg_executable, "-i", src, "-vf", "reverse", dst]
|
||||
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process.communicate()
|
||||
|
||||
|
||||
class Slide(Scene):
|
||||
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
|
||||
if MANIMGL:
|
||||
if not os.path.isdir("videos"):
|
||||
os.mkdir("videos")
|
||||
kwargs["file_writer_config"] = {
|
||||
"break_into_partial_movies": True,
|
||||
"output_directory": "",
|
||||
"write_to_movie": True,
|
||||
}
|
||||
|
||||
kwargs["preview"] = False
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.output_folder = output_folder
|
||||
self.slides = list()
|
||||
self.current_slide = 1
|
||||
@ -36,6 +43,35 @@ class Slide(Scene):
|
||||
self.loop_start_animation = None
|
||||
self.pause_start_animation = 0
|
||||
|
||||
@property
|
||||
def partial_movie_files(self):
|
||||
if MANIMGL:
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.file_writer.movie_file_extension,
|
||||
}
|
||||
return get_sorted_integer_files(
|
||||
self.file_writer.partial_movie_directory, **kwargs
|
||||
)
|
||||
else:
|
||||
return self.renderer.file_writer.partial_movie_files
|
||||
|
||||
@property
|
||||
def show_progress_bar(self):
|
||||
if MANIMGL:
|
||||
return getattr(super(Scene, self), "show_progress_bar", True)
|
||||
else:
|
||||
return config["progress_bar"] != "none"
|
||||
|
||||
@property
|
||||
def leave_progress_bar(self):
|
||||
if MANIMGL:
|
||||
return getattr(super(Scene, self), "leave_progress_bars", False)
|
||||
else:
|
||||
return config["progress_bar"] == "leave"
|
||||
|
||||
def play(self, *args, **kwargs):
|
||||
super().play(*args, **kwargs)
|
||||
self.current_animation += 1
|
||||
@ -72,14 +108,7 @@ class Slide(Scene):
|
||||
self.loop_start_animation = None
|
||||
self.pause_start_animation = self.current_animation
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
# We need to disable the caching limit since we rely on intermidiate files
|
||||
max_files_cached = config["max_files_cached"]
|
||||
config["max_files_cached"] = float("inf")
|
||||
|
||||
super().render(*args, **kwargs)
|
||||
|
||||
config["max_files_cached"] = max_files_cached
|
||||
def save_slides(self, use_cache=True):
|
||||
|
||||
if not os.path.exists(self.output_folder):
|
||||
os.mkdir(self.output_folder)
|
||||
@ -95,16 +124,19 @@ class Slide(Scene):
|
||||
|
||||
if not os.path.exists(scene_files_folder):
|
||||
os.mkdir(scene_files_folder)
|
||||
elif not use_cache:
|
||||
shutil.rmtree(scene_files_folder)
|
||||
os.mkdir(scene_files_folder)
|
||||
else:
|
||||
old_animation_files.update(os.listdir(scene_files_folder))
|
||||
|
||||
files = list()
|
||||
for src_file in tqdm(
|
||||
self.renderer.file_writer.partial_movie_files,
|
||||
self.partial_movie_files,
|
||||
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
|
||||
leave=config["progress_bar"] == "leave",
|
||||
leave=self.leave_progress_bar,
|
||||
ascii=True if platform.system() == "Windows" else None,
|
||||
disable=config["progress_bar"] == "none",
|
||||
disable=not self.show_progress_bar,
|
||||
):
|
||||
filename = os.path.basename(src_file)
|
||||
_hash, ext = os.path.splitext(filename)
|
||||
@ -140,6 +172,23 @@ class Slide(Scene):
|
||||
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
||||
)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
"""MANIMGL renderer"""
|
||||
super().run(*args, **kwargs)
|
||||
self.save_slides(use_cache=False)
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
"""MANIM render"""
|
||||
# We need to disable the caching limit since we rely on intermidiate files
|
||||
max_files_cached = config["max_files_cached"]
|
||||
config["max_files_cached"] = float("inf")
|
||||
|
||||
super().render(*args, **kwargs)
|
||||
|
||||
config["max_files_cached"] = max_files_cached
|
||||
|
||||
self.save_slides()
|
||||
|
||||
|
||||
class ThreeDSlide(Slide, ThreeDScene):
|
||||
pass
|
||||
|
Reference in New Issue
Block a user