diff --git a/manim/camera/moving_camera.py b/manim/camera/moving_camera.py index 3bb61120d2..7b73797f22 100644 --- a/manim/camera/moving_camera.py +++ b/manim/camera/moving_camera.py @@ -27,7 +27,11 @@ class MovingCamera(Camera): """A camera that follows and matches the size and position of its 'frame', a Rectangle (or similar Mobject). - The frame defines the region of space the camera displays and can move or resize dynamically. + Parameters + ---------- + frame + The frame defines the region of space the camera displays and can move or resize dynamically. + Frame is a Mobject (a rectangle), determining which region of space the camera displays .. SEEALSO:: @@ -42,9 +46,6 @@ def __init__( default_frame_stroke_width: int = 0, **kwargs: Any, ): - """Frame is a Mobject, (should almost certainly be a rectangle) - determining which region of space the camera displays - """ self.fixed_dimension = fixed_dimension self.default_frame_stroke_color = default_frame_stroke_color self.default_frame_stroke_width = default_frame_stroke_width diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 845fafd0b9..94c7eab088 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -155,6 +155,81 @@ class Scene: It is not recommended to override the ``__init__`` method in user Scenes. For code that should be ran before a Scene is rendered, use :meth:`Scene.setup` instead. + Parameters + ---------- + renderer + The renderer to use for this scene. + If `None`, a :class:`.CairoRenderer` is used by default, or + an :class:`.OpenGLRenderer` if `config.renderer` is set to + :attr:`.RendererType.OPENGL`, which can also be done at the time of running the code by + providing the flag: `--renderer=opengl`. + + camera_class + Which Camera class is to be used out of Camera, MovingCamera, ZoomedCamera, + MappingCamera, MultiCamera, OldMultiCamera, SplitScreenCamera or ThreeDCamera. + Default is Camera. + + always_update_mobjects + Normally mobjects only update when they have updaters attached. + Setting this `True` forces all mobjects to update every frame even without updaters. + Default value is `False`, which means that mobjects update only when an updater is attached to it. + + random_seed + Seed for Python's :mod:`random` module and :mod:`numpy.random`, + ensuring reproducible output. Falls back to ``config.seed`` if + not provided. + + skip_animations + If `True`, animations are not rendered frame by frame. + Instead, each :meth:`play` call just saves the very last state of the mobjects in memory. + Pixel data is written to disk not during any :meth:`play` call, + but only after the entire scene finishes, + and only if the output requires it (e.g. the `-s` flag). + + This is used to fast-forward through a scene efficiently in these 2 situations: + (1) When the `-s` flag(for saving only the last frame) is provided by the user at run time. + Explanation: When `-s` flag is provided, then, Manim needs to run through all the animations + to know what the final state of all the mobjects looks like, + but there's no point rendering all the frames of the video, just to grab the last one. + So it fast-forwards by skipping writing frames(except the very last frame of the entire scene). + Manim does this by setting :attr:`skip_animations` to `True` for every :meth:`.play` call in the entire scene. + When skip_animations = True, then only the frame writing is skipped and not the execution of play call. + Meaning that when skip_animations = True, all intermediate states are still computed + during the entire run_time of the respective play() call, + but none of them are written to disk and only the final state of the mobject is saved in memory. + Then after construct() finishes, scene_finished() renders and saves exactly one PNG of the final state. + + (2) Section-level skipping. + + Example :: + + self.next_section("intro") + self.play( + Create(Text("India")) + ) # skip_animations = False → frames written to disk + + self.next_section("main", skip_animations=True) + self.play( + Write(Text("Hi")) + ) # skip_animations = True → no frames written to disk + self.play( + GrowFromCenter(Circle()) + ) # skip_animations = True → no frames written to disk + + self.next_section("outro") + self.play( + FadeIn(Square()) + ) # skip_animations = False → frames written to disk + + + update_skipping_status() in cairo_renderer.py runs at the start of every + play() call and reads the current section's skip_animations flag to decide + whether to set skip_animations = True for that play() call. + So `skip_animations=True` is only set for play() calls belonging to that specific section, + not the entire scene. And unlike -s, no PNG is saved at the end. + The rest of the sections still render normally into the video, + and the skipped section simply contributes no frames to the output. + Examples -------- Override the :meth:`Scene.construct` method with your code. @@ -179,7 +254,6 @@ def __init__( self.always_update_mobjects = always_update_mobjects self.random_seed = random_seed if random_seed is not None else config.seed self.skip_animations = skip_animations - self.animations: list[Animation] | None = None self.stop_condition: Callable[[], bool] | None = None self.moving_mobjects: list[Mobject] = [] @@ -198,12 +272,20 @@ def __init__( self.mouse_press_callbacks: list[Callable[[], None]] = [] self.interactive_mode = False - if config.renderer == RendererType.OPENGL: + if ( + config.renderer == RendererType.OPENGL + ): # set when the flag --renderer=opengl is used for running the code or by setting renderer = opengl in the manim.cfg configuration file. # Items associated with interaction self.mouse_point = OpenGLPoint() self.mouse_drag_point = OpenGLPoint() if renderer is None: renderer = OpenGLRenderer() + elif not isinstance(renderer, OpenGLRenderer): + raise ValueError( + "Cannot use CairoRenderer when config.renderer is set to be OPENGL. " + "Either pass an OpenGLRenderer instance in the __init__ of Scene class, " + "or set renderer = cairo in your manim.cfg file, for the default CairoRenderer. " + ) if renderer is None: self.renderer: CairoRenderer | OpenGLRenderer = CairoRenderer( @@ -405,17 +487,25 @@ def update_self(self, dt: float) -> None: func(dt) def should_update_mobjects(self) -> bool: - """ - Returns True if the mobjects of this scene should be updated. - - In particular, this checks whether + """This method is called when a :class:`.Wait` animation is played. + Both of these code in the Example below, can trigger this method: + self.play(Wait()) # or + self.wait() + In particular, this method checks whether: - the :attr:`always_update_mobjects` attribute of :class:`.Scene` is set to ``True``, - the :class:`.Scene` itself has time-based updaters attached, + - the :class:`.Wait` animation has a stop condition attached, - any mobject in this :class:`.Scene` has time-based updaters attached. - This is only called when a single Wait animation is played. + If none of the above conditions are met, the wait is treated as a + static wait — meaning no frame-by-frame updates are performed. + + Returns + ------- + `True` if the mobjects of this scene should be updated. + `False` if the wait is static. """ assert self.animations is not None wait_animation = self.animations[0] @@ -454,11 +544,16 @@ def is_top_level(mobject: Mobject) -> bool: def get_mobject_family_members(self) -> list[Mobject]: """ - Returns list of family-members of all mobjects in scene. + Returns a list of family-members of all mobjects in the scene, + including the mobjects themselves and all their submobjects recursively. + If a Circle() and a VGroup(Rectangle(),Triangle()) were added, it returns not only the Circle(), Rectangle() and Triangle(), but also the VGroup() object. + When using the Cairo renderer, the list is sorted by each mobject's :attr:`z_index` attribute. + When using the OpenGL renderer, the list is returned in the order the mobjects were added. + Returns ------- list @@ -479,12 +574,12 @@ def get_mobject_family_members(self) -> list[Mobject]: def add(self, *mobjects: Mobject | OpenGLMobject) -> Self: """ Mobjects will be displayed, from background to - foreground in the order with which they are added. + foreground in the order with which they are added in the Scene, i.e. last added = frontmost. Parameters --------- *mobjects - Mobjects to add. + Mobjects to add to the Scene. Returns ------- @@ -633,8 +728,16 @@ def replace_in_list( def add_updater(self, func: Callable[[float], None]) -> None: """Add an update function to the scene. - The scene updater functions are run every frame, - and they are the last type of updaters to run. + In Manim there are 3 types of updaters: + (1) Mobject updaters, + (2) Mesh updaters(available only when using OpenGL renderer), + (3) Scene updaters. + + Every updater function runs every frame. When a Scene has multiple types + of updaters (mobject, mesh, and scene updaters), scene updaters are always + the last to run. If your Scene updater reads or reacts to Mobject's state or + Mesh's state, it will always see the Mobject's final state or Mesh's final state for that frame + and not its state from the previous frame. .. WARNING:: @@ -650,9 +753,8 @@ def add_updater(self, func: Callable[[float], None]) -> None: Parameters ---------- func - The updater function. It takes a float, which is the - time difference since the last update (usually equal - to the frame rate). + The updater function. It takes ``dt`` (a float), which is the + time elapsed since the last frame, and is equal to ``1 / frame_rate``. See also -------- @@ -681,17 +783,12 @@ def restructure_mobjects( to_remove: Sequence[Mobject], mobject_list_name: str = "mobjects", extract_families: bool = True, - ) -> Scene: + ) -> Self: """ - tl:wr - If your scene has a Group(), and you removed a mobject from the Group, - this dissolves the group and puts the rest of the mobjects directly - in self.mobjects or self.foreground_mobjects. - In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one - of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects - will be edited to contain other submobjects, but not m1, e.g. it will now - insert m2 and m3 to where the group once was. + of its submobjects is removed, e.g. scene.remove(m1), then the Group will be + dissolved and rest of the mobjects in the list of mobjects will be put directly + in self.mobjects or self.foreground_mobjects. Parameters ---------- @@ -758,7 +855,7 @@ def add_safe_mobjects_from_list( return new_mobjects # TODO, remove this, and calls to this - def add_foreground_mobjects(self, *mobjects: Mobject) -> Scene: + def add_foreground_mobjects(self, *mobjects: Mobject) -> Self: """ Adds mobjects to the foreground, and internally to the list foreground_mobjects, and mobjects. @@ -777,7 +874,7 @@ def add_foreground_mobjects(self, *mobjects: Mobject) -> Scene: self.add(*mobjects) return self - def add_foreground_mobject(self, mobject: Mobject) -> Scene: + def add_foreground_mobject(self, mobject: Mobject) -> Self: """ Adds a single mobject to the foreground, and internally to the list foreground_mobjects, and mobjects. @@ -794,7 +891,7 @@ def add_foreground_mobject(self, mobject: Mobject) -> Scene: """ return self.add_foreground_mobjects(mobject) - def remove_foreground_mobjects(self, *to_remove: Mobject) -> Scene: + def remove_foreground_mobjects(self, *to_remove: Mobject) -> Self: """ Removes mobjects from the foreground, and internally from the list foreground_mobjects. @@ -812,7 +909,7 @@ def remove_foreground_mobjects(self, *to_remove: Mobject) -> Scene: self.restructure_mobjects(to_remove, "foreground_mobjects") return self - def remove_foreground_mobject(self, mobject: Mobject) -> Scene: + def remove_foreground_mobject(self, mobject: Mobject) -> Self: """ Removes a single mobject from the foreground, and internally from the list foreground_mobjects. @@ -829,7 +926,7 @@ def remove_foreground_mobject(self, mobject: Mobject) -> Scene: """ return self.remove_foreground_mobjects(mobject) - def bring_to_front(self, *mobjects: Mobject) -> Scene: + def bring_to_front(self, *mobjects: Mobject) -> Self: """ Adds the passed mobjects to the scene again, pushing them to he front of the scene. @@ -848,7 +945,7 @@ def bring_to_front(self, *mobjects: Mobject) -> Scene: self.add(*mobjects) return self - def bring_to_back(self, *mobjects: Mobject) -> Scene: + def bring_to_back(self, *mobjects: Mobject) -> Self: """ Removes the mobject from the scene and adds them to the back of the scene. @@ -1152,26 +1249,30 @@ def play( subcaption_offset: float = 0, **kwargs: Any, ) -> None: - r"""Plays an animation in this scene. + """Plays an animation in this scene. Parameters ---------- args Animations to be played. + subcaption - The content of the external subcaption that should - be added during the animation. + The content of the external subcaption that should be added during the animation. + Subcaptions are written to a separate ``.srt`` subtitle file alongside the video, + which the video player overlays over the video at playback time. + subcaption_duration The duration for which the specified subcaption is added. If ``None`` (the default), the run time of the animation is taken. + subcaption_offset An offset (in seconds) for the start time of the added subcaption. + kwargs All other keywords are passed to the renderer. - """ # If we are in interactive embedded mode, make sure this is running on the main thread (required for OpenGL) if ( @@ -1192,7 +1293,9 @@ def play( start_time = self.time self.renderer.play(self, *args, **kwargs) - run_time = self.time - start_time + run_time = ( + self.time - start_time + ) # run_time here is the duration of the animation that just played when the above line run: self.renderer.play(self, *args, **kwargs). if subcaption: if subcaption_duration is None: subcaption_duration = run_time @@ -1265,6 +1368,21 @@ def wait_until( ) -> None: """Wait until a condition is satisfied, up to a given maximum duration. + Example:: + + class WaitUntilExample(Scene): + def construct(self): + dot = Dot(radius=1, color=RED) + self.add(dot) + dot.add_updater(lambda mob, dt: mob.shift(RIGHT * dt)) + + # wait until the dot reaches x = 3, but don't wait longer than 5 seconds + self.wait_until(stop_condition=lambda: dot.get_x() >= 3, max_time=5) + + dot.clear_updaters() # this line runs as soon as dot.get_x() >= 3 or if time > 5 seconds + self.play(dot.animate.set_color(GREEN).move_to(3 * UP + 3 * RIGHT)) + self.wait() + Parameters ---------- stop_condition