from pathlib import Path from typing import Any, Optional from manim import Scene, ThreeDScene, config from manim.renderer.opengl_renderer import OpenGLRenderer from manim.utils.color import rgba_to_color from ..config import BaseSlideConfig from .base import BaseSlide class Slide(BaseSlide, Scene): # type: ignore[misc] """ Inherits from :class:`Scene` and provides necessary tools for slides rendering. :param args: Positional arguments passed to scene object. :param pathlib.Path output_folder: Where the slide animation files should be written. :param kwargs: Keyword arguments passed to scene object. :cvar bool disable_caching: :data:`False`: Whether to disable the use of cached animation files. :cvar bool flush_cache: :data:`False`: Whether to flush the cache. Unlike with Manim, flushing is performed before rendering. :cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations. If set to :data:`False`, and no cached reversed animation exists (or caching is disabled) for a given slide, then the reversed animation will be simply the same as the original one, i.e., ``rev_file = file``, for the current slide config. :cvar typing.Optional[float] max_duration_before_split_reverse: :data:`4.0`: Maximum duration before of a video animation before it is reversed by splitting the file into smaller chunks. Generating reversed animations can require an important amount of memory (because the whole video needs to be kept in memory), and splitting the video into multiple chunks usually speeds up the process (because it can be done in parallel) while taking less memory. Set this to :data:`None` to disable splitting the file into chunks. :cvar typing.Optional[int] num_processes: :data:`None`: Number of processes to use for parallelizable operations. If :data:`None`, defaults to :func:`os.process_cpu_count`. This is currently used when generating reversed animations, and can increase memory consumption. """ def __init__(self, *args: Any, **kwargs: Any) -> None: # OpenGL renderer disables 'write_to_movie' by default # which is required for saving the animations config["write_to_movie"] = True super().__init__(*args, **kwargs) @property def _frame_shape(self) -> tuple[float, float]: if isinstance(self.renderer, OpenGLRenderer): return self.renderer.camera.frame_shape # type: ignore else: return ( self.renderer.camera.frame_height, self.renderer.camera.frame_width, ) @property def _frame_height(self) -> float: return self._frame_shape[0] @property def _frame_width(self) -> float: return self._frame_shape[1] @property def _background_color(self) -> str: if isinstance(self.renderer, OpenGLRenderer): return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore else: return self.renderer.camera.background_color.to_hex() # type: ignore @property def _resolution(self) -> tuple[int, int]: if isinstance(self.renderer, OpenGLRenderer): return self.renderer.get_pixel_shape() # type: ignore else: return ( self.renderer.camera.pixel_width, self.renderer.camera.pixel_height, ) @property def _partial_movie_files(self) -> list[Path]: # When rendering with -na,b (manim only) # the animations not in [a,b] will be skipped, # but animation before a will have a None source file. return [ Path(file) for file in self.renderer.file_writer.partial_movie_files if file is not None ] @property def _show_progress_bar(self) -> bool: return config["progress_bar"] != "none" # type: ignore @property def _leave_progress_bar(self) -> bool: return config["progress_bar"] == "leave" # type: ignore @property def _start_at_animation_number(self) -> Optional[int]: return config["from_animation_number"] # type: ignore def play(self, *args: Any, **kwargs: Any) -> None: """Overload 'self.play' and increment animation count.""" super().play(*args, **kwargs) if self._base_slide_config.skip_animations: # Manim will not render the animations, so we reset the animation # counter to the previous value self._current_animation -= 1 def next_section(self, *args: Any, **kwargs: Any) -> None: """ Alias to :meth:`next_slide`. :param args: Positional arguments to be passed to :meth:`next_slide`. :param kwargs: Keyword arguments to be passed to :meth:`next_slide`. .. attention:: This method is only available when using ``manim`` API. """ self.next_slide(*args, **kwargs) @BaseSlideConfig.wrapper("base_slide_config") def next_slide( self, *args: Any, base_slide_config: BaseSlideConfig, **kwargs: Any, ) -> None: Scene.next_section( self, *args, skip_animations=base_slide_config.skip_animations | self._skip_animations, **kwargs, ) BaseSlide.next_slide.__wrapped__( self, base_slide_config=base_slide_config, ) def render(self, *args: Any, **kwargs: Any) -> None: """MANIM renderer.""" # We need to disable the caching limit since we rely on intermediate files max_files_cached = config["max_files_cached"] config["max_files_cached"] = float("inf") flush_manim_cache = config["flush_cache"] if flush_manim_cache: # We need to postpone flushing *after* we saved slides config["flush_cache"] = False super().render(*args, **kwargs) config["max_files_cached"] = max_files_cached self._save_slides( use_cache=not (config["disable_caching"] or self.disable_caching), flush_cache=(config["flush_cache"] or self.flush_cache), skip_reversing=self.skip_reversing, ) if flush_manim_cache: self.renderer.file_writer.flush_cache_directory() class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc] """ Inherits from :class:`Slide` and :class:`ThreeDScene` and provide necessary tools for slides rendering. Examples -------- .. manim-slides:: ThreeDExample from manim import * from manim_slides import ThreeDSlide class ThreeDExample(ThreeDSlide): def construct(self): title = Text("A 2D Text") self.play(FadeIn(title)) self.next_slide() sphere = Sphere([0, 0, -3]) self.move_camera(phi=PI/3, theta=-PI/4, distance=7) self.play( GrowFromCenter(sphere), Transform(title, Text("A 3D Text")) ) self.next_slide() bye = Text("Bye!") self.next_slide(loop=True) self.wipe( self.mobjects_without_canvas, [bye], direction=UP ) self.wait(.5) self.wipe( self.mobjects_without_canvas, [title, sphere], direction=DOWN ) self.wait(.5) self.next_slide() self.play(*[FadeOut(mobject) for mobject in self.mobjects]) """ pass