From ac5582753c3201cbd46c064564fd309a2c498787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Eertmans?= Date: Sat, 10 Sep 2022 22:53:35 +0200 Subject: [PATCH 1/3] feat: add support for manimgl --- .gitignore | 4 ++ README.md | 14 +++++-- example.py | 4 +- manim_slides/manim.py | 40 +++++++++++++++++++ manim_slides/present.py | 8 ++-- manim_slides/slide.py | 85 ++++++++++++++++++++++++++++++++--------- 6 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 manim_slides/manim.py diff --git a/.gitignore b/.gitignore index 70e52faa..31f7e253 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ __pycache__/ slides/ .manim-slides.json + +videos/ + +images/ diff --git a/README.md b/README.md index 74208b45..0ca2f526 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ ![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides) # 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 diff --git a/example.py b/example.py index f3b47351..2938047d 100644 --- a/example.py +++ b/example.py @@ -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 diff --git a/manim_slides/manim.py b/manim_slides/manim.py new file mode 100644 index 00000000..5fc756ea --- /dev/null +++ b/manim_slides/manim.py @@ -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 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 @@ def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs): 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 @@ def end_loop(self): 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 @@ def render(self, *args, **kwargs): 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 @@ def render(self, *args, **kwargs): 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 From 85c295a2c1ad6a37307a59d82406c20b140e4035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Eertmans?= Date: Sat, 10 Sep 2022 22:53:35 +0200 Subject: [PATCH 2/3] feat: add support for manimgl --- .gitignore | 4 ++ README.md | 14 +++++-- example.py | 4 +- manim_slides/manim.py | 40 +++++++++++++++++++ manim_slides/present.py | 8 ++-- manim_slides/slide.py | 85 ++++++++++++++++++++++++++++++++--------- 6 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 manim_slides/manim.py diff --git a/.gitignore b/.gitignore index 70e52faa..31f7e253 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ __pycache__/ slides/ .manim-slides.json + +videos/ + +images/ diff --git a/README.md b/README.md index 4f66aa98..0ca2f526 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ ![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides) # Manim Slides -Tool for live presentations extending [manim-community](https://www.manim.community/)'s capabilities. 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 diff --git a/example.py b/example.py index f3b47351..2938047d 100644 --- a/example.py +++ b/example.py @@ -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 diff --git a/manim_slides/manim.py b/manim_slides/manim.py new file mode 100644 index 00000000..5fc756ea --- /dev/null +++ b/manim_slides/manim.py @@ -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 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 @@ def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs): 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 @@ def end_loop(self): 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 @@ def render(self, *args, **kwargs): 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 @@ def render(self, *args, **kwargs): 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 From cda304fef0111d2eb7926f192ea4f9ed4ad831b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 10 Sep 2022 20:54:37 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim_slides/manim.py | 11 ++++++++--- manim_slides/present.py | 8 ++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/manim_slides/manim.py b/manim_slides/manim.py index 5fc756ea..97fa217c 100644 --- a/manim_slides/manim.py +++ b/manim_slides/manim.py @@ -1,5 +1,5 @@ -from importlib.util import find_spec import sys +from importlib.util import find_spec MANIM_PACKAGE_NAME = "manim" MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None @@ -11,7 +11,10 @@ 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") + + 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: @@ -21,7 +24,9 @@ MANIM = False MANIMGL = True else: - raise ImportError("Either manim (community) or manimgl (3b1b) package must be installed") + raise ImportError( + "Either manim (community) or manimgl (3b1b) package must be installed" + ) FFMPEG_BIN = None diff --git a/manim_slides/present.py b/manim_slides/present.py index 1aaeedb6..fc2bd2ee 100644 --- a/manim_slides/present.py +++ b/manim_slides/present.py @@ -377,12 +377,16 @@ def value_proc(value: str): indices = list(map(int, value.strip().replace(" ", "").split(","))) if not all(map(lambda i: 0 < i <= len(scene_choices), indices)): - raise click.UsageError("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 click.UsageError("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: