Skip to content

Separate :class:.Animation and :class:.Scene #3685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
3 changes: 3 additions & 0 deletions example_scenes/new_test_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def progress_through_animations(animations):
win = Window(
width=1920,
height=1080,
fullscreen=True,
vsync=True,
config=Config(double_buffer=True, samples=0),
)
Expand Down Expand Up @@ -151,6 +152,8 @@ def on_resize(width, height):
if not is_finished:
if virtual_time >= run_time:
animation.finish()
buffer = str(animation.buffer)
print(f"{buffer = }")
has_finished = True
else:
animation.update_mobjects(dt)
Expand Down
107 changes: 46 additions & 61 deletions manim/animation/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
from ..mobject.mobject import Mobject
from ..mobject.opengl import opengl_mobject
from ..utils.rate_functions import linear, smooth
from .protocol import AnimationProtocol
from .scene_buffer import SceneBuffer

__all__ = ["Animation", "Wait", "override_animation"]


from copy import deepcopy
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Callable, Iterable, Sequence, TypeVar

if TYPE_CHECKING:
from manim.scene.scene import Scene
from typing_extensions import Self


DEFAULT_ANIMATION_RUN_TIME: float = 1.0
Expand Down Expand Up @@ -127,17 +128,17 @@ def __new__(

def __init__(
self,
mobject: Mobject | None,
mobject: OpenGLMobject | None,
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
rate_func: Callable[[float], float] = smooth,
reverse_rate_function: bool = False,
name: str = None,
remover: bool = False, # remove a mobject from the screen?
name: str | None = None,
remover: bool = False, # remove a mobject from the screen at end of animation
suspend_mobject_updating: bool = True,
introducer: bool = False,
*,
_on_finish: Callable[[], None] = lambda _: None,
_on_finish: Callable[[SceneBuffer], object] = lambda _: None,
**kwargs,
) -> None:
self._typecheck_input(mobject)
Expand All @@ -149,15 +150,16 @@ def __init__(
self.introducer: bool = introducer
self.suspend_mobject_updating: bool = suspend_mobject_updating
self.lag_ratio: float = lag_ratio
self._on_finish: Callable[[Scene], None] = _on_finish
self._on_finish = _on_finish
self.buffer = SceneBuffer()
if config["renderer"] == RendererType.OPENGL:
self.starting_mobject: OpenGLMobject = OpenGLMobject()
self.mobject: OpenGLMobject = (
mobject if mobject is not None else OpenGLMobject()
)
else:
self.starting_mobject: Mobject = Mobject()
self.mobject: Mobject = mobject if mobject is not None else Mobject()
# else:
# self.starting_mobject: Mobject = Mobject()
# self.mobject: Mobject = mobject if mobject is not None else Mobject()
if kwargs:
logger.debug("Animation received extra kwargs: %s", kwargs)

Expand All @@ -169,7 +171,7 @@ def __init__(
),
)

def _typecheck_input(self, mobject: Mobject | None) -> None:
def _typecheck_input(self, mobject: Mobject | OpenGLMobject | None) -> None:
if mobject is None:
logger.debug("Animation with empty mobject")
elif not isinstance(mobject, (Mobject, OpenGLMobject)):
Expand Down Expand Up @@ -213,10 +215,10 @@ def begin(self) -> None:
self.mobject.suspend_updating()
self.interpolate(0)

if self.is_introducer():
self.buffer.add(self.mobject)

def finish(self) -> None:
# TODO: begin and finish should require a scene as parameter.
# That way Animation.clean_up_from_screen and Scene.add_mobjects_from_animations
# could be removed as they fulfill basically the same purpose.
"""Finish the animation.

This method gets called when the animation is over.
Expand All @@ -226,44 +228,20 @@ def finish(self) -> None:
if self.suspend_mobject_updating and self.mobject is not None:
self.mobject.resume_updating()

def clean_up_from_scene(self, scene: Scene) -> None:
"""Clean up the :class:`~.Scene` after finishing the animation.

This includes to :meth:`~.Scene.remove` the Animation's
:class:`~.Mobject` if the animation is a remover.

Parameters
----------
scene
The scene the animation should be cleaned up from.
"""
self._on_finish(scene)
self._on_finish(self.buffer)
if self.is_remover():
scene.remove(self.mobject)

def _setup_scene(self, scene: Scene) -> None:
"""Setup up the :class:`~.Scene` before starting the animation.

This includes to :meth:`~.Scene.add` the Animation's
:class:`~.Mobject` if the animation is an introducer.

Parameters
----------
scene
The scene the animation should be cleaned up from.
"""
if scene is None:
return
if (
self.is_introducer()
and self.mobject not in scene.get_mobject_family_members()
):
scene.add(self.mobject)
self.buffer.remove(self.mobject)

def create_starting_mobject(self) -> Mobject:
# Keep track of where the mobject starts
return self.mobject.copy()

def get_all_animations(self) -> tuple[Animation, ...]:
"""This method is to implement an animation protocol, and
is more useful in places like :class:`.AnimationGroup`
"""
return (self,)

def get_all_mobjects(self) -> Sequence[Mobject]:
"""Get all mobjects involved in the animation.

Expand Down Expand Up @@ -294,6 +272,15 @@ def update_mobjects(self, dt: float) -> None:
for mob in self.get_all_mobjects_to_update():
mob.update(dt)

def process_subanimation_buffer(self, buffer: SceneBuffer):
"""
This is used in animations that are proxies around
other animations
"""
self.buffer.add(*buffer.to_add)
self.buffer.remove(*buffer.to_remove)
self.buffer.clear()

def get_all_mobjects_to_update(self) -> list[Mobject]:
"""Get all mobjects to be updated during the animation.

Expand All @@ -305,9 +292,9 @@ def get_all_mobjects_to_update(self) -> list[Mobject]:
# The surrounding scene typically handles
# updating of self.mobject. Besides, in
# most cases its updating is suspended anyway
return list(filter(lambda m: m is not self.mobject, self.get_all_mobjects()))
return [m for m in self.get_all_mobjects() if m is not self.mobject]

def copy(self) -> Animation:
def copy(self) -> Self:
"""Create a copy of the animation.

Returns
Expand Down Expand Up @@ -356,7 +343,7 @@ def interpolate_submobject(
alpha: float,
) -> Animation:
# Typically implemented by subclass
pass
raise NotImplementedError()

def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float:
"""Get the animation progress of any submobjects subanimation.
Expand Down Expand Up @@ -422,7 +409,7 @@ def get_run_time(self) -> float:
def set_rate_func(
self,
rate_func: Callable[[float], float],
) -> Animation:
) -> Self:
"""Set the rate function of the animation.

Parameters
Expand Down Expand Up @@ -451,7 +438,7 @@ def get_rate_func(
"""
return self.rate_func

def set_name(self, name: str) -> Animation:
def set_name(self, name: str) -> Self:
"""Set the name of the animation.

Parameters
Expand Down Expand Up @@ -489,7 +476,9 @@ def is_introducer(self) -> bool:


def prepare_animation(
anim: Animation | mobject._AnimationBuilder,
anim: AnimationProtocol
| mobject._AnimationBuilder
| opengl_mobject._AnimationBuilder,
) -> Animation:
r"""Returns either an unchanged animation, or the animation built
from a passed animation factory.
Expand Down Expand Up @@ -517,10 +506,7 @@ def prepare_animation(
TypeError: Object 42 cannot be converted to an animation

"""
if isinstance(anim, mobject._AnimationBuilder):
return anim.build()

if isinstance(anim, opengl_mobject._AnimationBuilder):
if isinstance(anim, (mobject._AnimationBuilder, opengl_mobject._AnimationBuilder)):
return anim.build()

if isinstance(anim, Animation):
Expand Down Expand Up @@ -576,9 +562,6 @@ def begin(self) -> None:
def finish(self) -> None:
pass

def clean_up_from_scene(self, scene: Scene) -> None:
pass

def update_mobjects(self, dt: float) -> None:
pass

Expand Down Expand Up @@ -625,7 +608,9 @@ def construct(self):

"""

def decorator(func):
_F = TypeVar("_F", bound=Callable)

def decorator(func: _F) -> _F:
func._override_animation = animation_class
return func

Expand Down
14 changes: 9 additions & 5 deletions manim/animation/changing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

__all__ = ["AnimatedBoundary", "TracedPath"]

from typing import Callable
from typing import TYPE_CHECKING, Callable

if TYPE_CHECKING:
import numpy.typing as npt

from manim._config import config
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.color import (
Expand Down Expand Up @@ -61,7 +63,7 @@ def __init__(
]
self.add(*self.boundary_copies)
self.total_time = 0
self.add_updater(lambda m, dt: self.update_boundary_copies(dt))
self.add_updater(lambda _, dt: self.update_boundary_copies(dt))

def update_boundary_copies(self, dt):
# Not actual time, but something which passes at
Expand Down Expand Up @@ -143,7 +145,9 @@ def construct(self):

def __init__(
self,
traced_point_func: Callable,
traced_point_func: Callable[
[], npt.NDArray[npt.float]
], # TODO: Replace with Callable[[], Point3D]
stroke_width: float = 2,
stroke_color: ParsableManimColor | None = WHITE,
dissipating_time: float | None = None,
Expand All @@ -155,7 +159,7 @@ def __init__(
self.time = 1 if self.dissipating_time else None
self.add_updater(self.update_path)

def update_path(self, mob, dt):
def update_path(self, _mob, dt):
new_point = self.traced_point_func()
if not self.has_points():
self.start_new_path(new_point)
Expand Down
40 changes: 15 additions & 25 deletions manim/animation/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
from ..animation.animation import Animation, prepare_animation
from ..constants import RendererType
from ..mobject.mobject import Group, Mobject
from ..scene.scene import Scene
from ..utils.iterables import remove_list_redundancies
from ..utils.rate_functions import linear

if TYPE_CHECKING:
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup

from ..mobject.types.vectorized_mobject import VGroup
from .scene_buffer import SceneBuffer

__all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]

Expand Down Expand Up @@ -80,28 +80,24 @@ def __init__(
def get_all_mobjects(self) -> Sequence[Mobject]:
return list(self.group)

def get_all_animations(self) -> tuple[Animation, ...]:
return tuple(self.animations)

def begin(self) -> None:
super().begin()
if self.suspend_mobject_updating:
self.group.suspend_updating()
for anim in self.animations:
anim.begin()

def _setup_scene(self, scene) -> None:
for anim in self.animations:
anim._setup_scene(scene)

def finish(self) -> None:
for anim in self.animations:
if self.remover:
anim.remover = self.remover
anim.finish()
self.process_subanimation_buffer(anim.buffer)

if self.suspend_mobject_updating:
self.group.resume_updating()

def clean_up_from_scene(self, scene: Scene) -> None:
self._on_finish(scene)
for anim in self.animations:
if self.remover:
anim.remover = self.remover
anim.clean_up_from_scene(scene)
self._on_finish(self.buffer)

def update_mobjects(self, dt: float) -> None:
for anim in self.animations:
Expand Down Expand Up @@ -198,6 +194,10 @@ def begin(self) -> None:
assert len(self.animations) > 0
self.update_active_animation(0)

for anim in self.animations:
if not anim.is_introducer() and anim.mobject is not None:
self.buffer.add(anim.mobject)

def finish(self) -> None:
while self.active_animation is not None:
self.next_animation()
Expand All @@ -206,16 +206,6 @@ def update_mobjects(self, dt: float) -> None:
if self.active_animation:
self.active_animation.update_mobjects(dt)

def _setup_scene(self, scene) -> None:
if scene is None:
return
if self.is_introducer():
for anim in self.animations:
if not anim.is_introducer() and anim.mobject is not None:
scene.add(anim.mobject)

self.scene = scene

def update_active_animation(self, index: int) -> None:
self.active_index = index
if index >= len(self.animations):
Expand All @@ -224,7 +214,6 @@ def update_active_animation(self, index: int) -> None:
self.active_end_time: float | None = None
else:
self.active_animation = self.animations[index]
self.active_animation._setup_scene(self.scene)
self.active_animation.begin()
self.active_start_time = self.anims_with_timings[index][1]
self.active_end_time = self.anims_with_timings[index][2]
Expand All @@ -236,6 +225,7 @@ def next_animation(self) -> None:
"""
if self.active_animation is not None:
self.active_animation.finish()
self.process_subanimation_buffer(self.active_animation.buffer)
self.update_active_animation(self.active_index + 1)

def interpolate(self, alpha: float) -> None:
Expand Down
Loading