mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 03:05:21 +08:00
324 lines
10 KiB
Python
324 lines
10 KiB
Python
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
from typing import Any, List, Optional
|
|
from warnings import warn
|
|
|
|
from tqdm import tqdm
|
|
|
|
from .config import PresentationConfig, SlideConfig, SlideType
|
|
from .defaults import FOLDER_PATH
|
|
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
|
|
|
|
|
def reverse_video_file(src: str, dst: str) -> None:
|
|
"""Reverses a video file, writting the result to `dst`."""
|
|
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
|
logger.debug(" ".join(command))
|
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
output, error = process.communicate()
|
|
|
|
if output:
|
|
logger.debug(output.decode())
|
|
|
|
if error:
|
|
logger.debug(error.decode())
|
|
|
|
|
|
class Slide(Scene): # type:ignore
|
|
"""
|
|
Inherits from :class:`manim.scene.scene.Scene` or :class:`manimlib.scene.scene.Scene` and provide necessary tools for slides rendering.
|
|
"""
|
|
|
|
def __init__(
|
|
self, *args: Any, output_folder: str = FOLDER_PATH, **kwargs: Any
|
|
) -> None:
|
|
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[SlideConfig] = []
|
|
self.current_slide = 1
|
|
self.current_animation = 0
|
|
self.loop_start_animation: Optional[int] = None
|
|
self.pause_start_animation = 0
|
|
|
|
@property
|
|
def partial_movie_files(self) -> List[str]:
|
|
"""Returns a list of partial movie files, a.k.a animations."""
|
|
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) -> bool:
|
|
"""Returns True if progress bar should be displayed."""
|
|
if MANIMGL:
|
|
return getattr(super(Scene, self), "show_progress_bar", True)
|
|
else:
|
|
return config["progress_bar"] != "none"
|
|
|
|
@property
|
|
def leave_progress_bar(self) -> bool:
|
|
"""Returns True if progress bar should be left after completed."""
|
|
if MANIMGL:
|
|
return getattr(super(Scene, self), "leave_progress_bars", False)
|
|
else:
|
|
return config["progress_bar"] == "leave"
|
|
|
|
def play(self, *args: Any, **kwargs: Any) -> None:
|
|
"""Overloads `self.play` and increment animation count."""
|
|
super().play(*args, **kwargs)
|
|
self.current_animation += 1
|
|
|
|
def next_slide(self):
|
|
"""
|
|
Creates a new slide with previous animations.
|
|
|
|
This usually means that the user will need to press some key before the
|
|
next slide is played. By default, this is the right arrow key.
|
|
|
|
|
|
.. note::
|
|
|
|
Calls to :func:`next_slide` at the very beginning or at the end are
|
|
not needed, since they are automatically added.
|
|
|
|
.. warning::
|
|
|
|
This is not allowed to call :func:`next_slide` inside a loop.
|
|
|
|
Examples
|
|
--------
|
|
|
|
The following contains 3 slides:
|
|
|
|
#. the first with nothing on it;
|
|
#. the second with "Hello World!" fading in;
|
|
#. and the last with the text fading out;
|
|
|
|
.. code-block:: python
|
|
|
|
from manim import *
|
|
from manim_slides import Slide
|
|
|
|
class Example(Slide):
|
|
def construct(self):
|
|
text = Text("Hello World!")
|
|
|
|
self.next_slide()
|
|
self.play(FadeIn(text))
|
|
|
|
self.next_slide()
|
|
self.play(FadeOut(text))
|
|
"""
|
|
assert (
|
|
self.loop_start_animation is None
|
|
), "You cannot call `self.next_slide()` inside a loop"
|
|
|
|
self.slides.append(
|
|
SlideConfig(
|
|
type=SlideType.slide,
|
|
start_animation=self.pause_start_animation,
|
|
end_animation=self.current_animation,
|
|
number=self.current_slide,
|
|
)
|
|
)
|
|
self.current_slide += 1
|
|
self.pause_start_animation = self.current_animation
|
|
|
|
def pause(self) -> None:
|
|
"""
|
|
Creates a new slide with previous animations.
|
|
|
|
.. deprecated:: 4.10.0
|
|
Use :func:`next_slide` instead.
|
|
"""
|
|
warn(
|
|
"`self.pause()` is deprecated. Use `self.next_slide()` instead.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
self.next_slide()
|
|
|
|
def add_last_slide(self) -> None:
|
|
"""Adds a 'last' slide to the end of slides."""
|
|
|
|
if (
|
|
len(self.slides) > 0
|
|
and self.current_animation == self.slides[-1].end_animation
|
|
):
|
|
self.slides[-1].type = SlideType.last
|
|
return
|
|
|
|
self.slides.append(
|
|
SlideConfig(
|
|
type=SlideType.last,
|
|
start_animation=self.pause_start_animation,
|
|
end_animation=self.current_animation,
|
|
number=self.current_slide,
|
|
)
|
|
)
|
|
|
|
def start_loop(self) -> None:
|
|
"""
|
|
Starts a loop. End it with :func:`end_loop`.
|
|
|
|
A loop will automatically replay the slide, i.e., everything between
|
|
:func:`start_loop` and :func:`end_loop`, upon reaching end.
|
|
|
|
Examples
|
|
--------
|
|
|
|
The following contains one slide that will loop endlessly.
|
|
|
|
.. code-block:: python
|
|
|
|
from manim import *
|
|
from manim_slides import Slide
|
|
|
|
class Example(Slide):
|
|
def construct(self):
|
|
dot = Dot(color=BLUE)
|
|
|
|
self.start_loop()
|
|
|
|
self.play(Indicate(dot))
|
|
|
|
self.end_loop()
|
|
"""
|
|
assert self.loop_start_animation is None, "You cannot nest loops"
|
|
self.loop_start_animation = self.current_animation
|
|
|
|
def end_loop(self) -> None:
|
|
"""Ends an existing loop. See :func:`start_loop` for more details."""
|
|
assert (
|
|
self.loop_start_animation is not None
|
|
), "You have to start a loop before ending it"
|
|
self.slides.append(
|
|
SlideConfig(
|
|
type=SlideType.loop,
|
|
start_animation=self.loop_start_animation,
|
|
end_animation=self.current_animation,
|
|
number=self.current_slide,
|
|
)
|
|
)
|
|
self.current_slide += 1
|
|
self.loop_start_animation = None
|
|
self.pause_start_animation = self.current_animation
|
|
|
|
def save_slides(self, use_cache: bool = True) -> None:
|
|
"""
|
|
Saves slides, optionally using cached files.
|
|
|
|
Note that cached files only work with Manim.
|
|
"""
|
|
self.add_last_slide()
|
|
|
|
if not os.path.exists(self.output_folder):
|
|
os.mkdir(self.output_folder)
|
|
|
|
files_folder = os.path.join(self.output_folder, "files")
|
|
if not os.path.exists(files_folder):
|
|
os.mkdir(files_folder)
|
|
|
|
scene_name = type(self).__name__
|
|
scene_files_folder = os.path.join(files_folder, scene_name)
|
|
|
|
old_animation_files = set()
|
|
|
|
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 = []
|
|
for src_file in tqdm(
|
|
self.partial_movie_files,
|
|
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
|
|
leave=self.leave_progress_bar,
|
|
ascii=True if platform.system() == "Windows" else None,
|
|
disable=not self.show_progress_bar,
|
|
):
|
|
filename = os.path.basename(src_file)
|
|
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
|
|
|
|
dst_file = os.path.join(scene_files_folder, filename)
|
|
# We only copy animation if it was not present
|
|
if filename in old_animation_files:
|
|
old_animation_files.remove(filename)
|
|
else:
|
|
shutil.copyfile(src_file, dst_file)
|
|
|
|
# We only reverse video if it was not present
|
|
if rev_filename in old_animation_files:
|
|
old_animation_files.remove(rev_filename)
|
|
else:
|
|
rev_file = os.path.join(scene_files_folder, rev_filename)
|
|
reverse_video_file(src_file, rev_file)
|
|
|
|
files.append(dst_file)
|
|
|
|
logger.info(
|
|
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
|
|
)
|
|
|
|
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
|
|
|
|
with open(slide_path, "w") as f:
|
|
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
|
|
|
|
logger.info(
|
|
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
|
)
|
|
|
|
def run(self, *args: Any, **kwargs: Any) -> None:
|
|
"""MANIMGL renderer"""
|
|
super().run(*args, **kwargs)
|
|
self.save_slides(use_cache=False)
|
|
|
|
def render(self, *args: Any, **kwargs: Any) -> None:
|
|
"""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): # type: ignore
|
|
"""
|
|
Inherits from :class:`Slide` and :class:`manim.scene.three_d_scene.ThreeDScene` or :class:`manimlib.scene.three_d_scene.ThreeDScene` and provide necessary tools for slides rendering.
|
|
|
|
.. note:: ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
|
|
"""
|
|
|
|
pass
|