Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions manim/camera/moving_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -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
Expand Down
188 changes: 153 additions & 35 deletions manim/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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] = []
Expand All @@ -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(
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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
-------
Expand Down Expand Up @@ -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::

Expand All @@ -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
--------
Expand Down Expand Up @@ -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
----------
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading