Skip to content

Commit 6ae8526

Browse files
Separate Animation and Scene using SceneBuffer
1 parent c6dfc15 commit 6ae8526

File tree

4 files changed

+168
-82
lines changed

4 files changed

+168
-82
lines changed

manim/animation/animation.py

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@
1111
from ..mobject.mobject import Mobject
1212
from ..mobject.opengl import opengl_mobject
1313
from ..utils.rate_functions import linear, smooth
14+
from .scene_buffer import SceneBuffer
15+
from .protocol import AnimationProtocol
1416

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

1719

1820
from copy import deepcopy
19-
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
21+
from typing import TYPE_CHECKING, Callable, Iterable, TypeVar, Sequence
2022

2123
if TYPE_CHECKING:
22-
from manim.scene.scene import Scene
24+
from typing_extensions import Self
2325

2426

2527
DEFAULT_ANIMATION_RUN_TIME: float = 1.0
@@ -127,17 +129,17 @@ def __new__(
127129

128130
def __init__(
129131
self,
130-
mobject: Mobject | None,
132+
mobject: OpenGLMobject | None,
131133
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
132134
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
133135
rate_func: Callable[[float], float] = smooth,
134136
reverse_rate_function: bool = False,
135-
name: str = None,
136-
remover: bool = False, # remove a mobject from the screen?
137+
name: str | None = None,
138+
remover: bool = False, # remove a mobject from the screen at end of animation
137139
suspend_mobject_updating: bool = True,
138140
introducer: bool = False,
139141
*,
140-
_on_finish: Callable[[], None] = lambda _: None,
142+
_on_finish: Callable[[SceneBuffer], object] = lambda _: None,
141143
**kwargs,
142144
) -> None:
143145
self._typecheck_input(mobject)
@@ -149,15 +151,16 @@ def __init__(
149151
self.introducer: bool = introducer
150152
self.suspend_mobject_updating: bool = suspend_mobject_updating
151153
self.lag_ratio: float = lag_ratio
152-
self._on_finish: Callable[[Scene], None] = _on_finish
154+
self._on_finish = _on_finish
155+
self.buffer = SceneBuffer()
153156
if config["renderer"] == RendererType.OPENGL:
154157
self.starting_mobject: OpenGLMobject = OpenGLMobject()
155158
self.mobject: OpenGLMobject = (
156159
mobject if mobject is not None else OpenGLMobject()
157160
)
158-
else:
159-
self.starting_mobject: Mobject = Mobject()
160-
self.mobject: Mobject = mobject if mobject is not None else Mobject()
161+
# else:
162+
# self.starting_mobject: Mobject = Mobject()
163+
# self.mobject: Mobject = mobject if mobject is not None else Mobject()
161164
if kwargs:
162165
logger.debug("Animation received extra kwargs: %s", kwargs)
163166

@@ -169,7 +172,7 @@ def __init__(
169172
),
170173
)
171174

172-
def _typecheck_input(self, mobject: Mobject | None) -> None:
175+
def _typecheck_input(self, mobject: Mobject | OpenGLMobject | None) -> None:
173176
if mobject is None:
174177
logger.debug("Animation with empty mobject")
175178
elif not isinstance(mobject, (Mobject, OpenGLMobject)):
@@ -213,10 +216,10 @@ def begin(self) -> None:
213216
self.mobject.suspend_updating()
214217
self.interpolate(0)
215218

219+
if self.is_introducer():
220+
self.buffer.add(self.mobject)
221+
216222
def finish(self) -> None:
217-
# TODO: begin and finish should require a scene as parameter.
218-
# That way Animation.clean_up_from_screen and Scene.add_mobjects_from_animations
219-
# could be removed as they fulfill basically the same purpose.
220223
"""Finish the animation.
221224
222225
This method gets called when the animation is over.
@@ -226,44 +229,20 @@ def finish(self) -> None:
226229
if self.suspend_mobject_updating and self.mobject is not None:
227230
self.mobject.resume_updating()
228231

229-
def clean_up_from_scene(self, scene: Scene) -> None:
230-
"""Clean up the :class:`~.Scene` after finishing the animation.
231-
232-
This includes to :meth:`~.Scene.remove` the Animation's
233-
:class:`~.Mobject` if the animation is a remover.
234-
235-
Parameters
236-
----------
237-
scene
238-
The scene the animation should be cleaned up from.
239-
"""
240-
self._on_finish(scene)
232+
self._on_finish(self.buffer)
241233
if self.is_remover():
242-
scene.remove(self.mobject)
243-
244-
def _setup_scene(self, scene: Scene) -> None:
245-
"""Setup up the :class:`~.Scene` before starting the animation.
246-
247-
This includes to :meth:`~.Scene.add` the Animation's
248-
:class:`~.Mobject` if the animation is an introducer.
249-
250-
Parameters
251-
----------
252-
scene
253-
The scene the animation should be cleaned up from.
254-
"""
255-
if scene is None:
256-
return
257-
if (
258-
self.is_introducer()
259-
and self.mobject not in scene.get_mobject_family_members()
260-
):
261-
scene.add(self.mobject)
234+
self.buffer.remove(self.mobject)
262235

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

240+
def get_all_animations(self) -> tuple[Animation, ...]:
241+
"""This method is to implement an animation protocol, and
242+
is more useful in places like :class:`.AnimationGroup`
243+
"""
244+
return (self,)
245+
267246
def get_all_mobjects(self) -> Sequence[Mobject]:
268247
"""Get all mobjects involved in the animation.
269248
@@ -305,9 +284,9 @@ def get_all_mobjects_to_update(self) -> list[Mobject]:
305284
# The surrounding scene typically handles
306285
# updating of self.mobject. Besides, in
307286
# most cases its updating is suspended anyway
308-
return list(filter(lambda m: m is not self.mobject, self.get_all_mobjects()))
287+
return [m for m in self.get_all_mobjects() if m is not self.mobject]
309288

310-
def copy(self) -> Animation:
289+
def copy(self) -> Self:
311290
"""Create a copy of the animation.
312291
313292
Returns
@@ -422,7 +401,7 @@ def get_run_time(self) -> float:
422401
def set_rate_func(
423402
self,
424403
rate_func: Callable[[float], float],
425-
) -> Animation:
404+
) -> Self:
426405
"""Set the rate function of the animation.
427406
428407
Parameters
@@ -451,7 +430,7 @@ def get_rate_func(
451430
"""
452431
return self.rate_func
453432

454-
def set_name(self, name: str) -> Animation:
433+
def set_name(self, name: str) -> Self:
455434
"""Set the name of the animation.
456435
457436
Parameters
@@ -489,7 +468,7 @@ def is_introducer(self) -> bool:
489468

490469

491470
def prepare_animation(
492-
anim: Animation | mobject._AnimationBuilder,
471+
anim: AnimationProtocol | mobject._AnimationBuilder | opengl_mobject._AnimationBuilder,
493472
) -> Animation:
494473
r"""Returns either an unchanged animation, or the animation built
495474
from a passed animation factory.
@@ -517,10 +496,7 @@ def prepare_animation(
517496
TypeError: Object 42 cannot be converted to an animation
518497
519498
"""
520-
if isinstance(anim, mobject._AnimationBuilder):
521-
return anim.build()
522-
523-
if isinstance(anim, opengl_mobject._AnimationBuilder):
499+
if isinstance(anim, (mobject._AnimationBuilder, opengl_mobject._AnimationBuilder)):
524500
return anim.build()
525501

526502
if isinstance(anim, Animation):
@@ -576,7 +552,7 @@ def begin(self) -> None:
576552
def finish(self) -> None:
577553
pass
578554

579-
def clean_up_from_scene(self, scene: Scene) -> None:
555+
def clean_up_from_scene(self, scene: SceneBuffer) -> None:
580556
pass
581557

582558
def update_mobjects(self, dt: float) -> None:
@@ -625,7 +601,9 @@ def construct(self):
625601
626602
"""
627603

628-
def decorator(func):
604+
_F = TypeVar("_F", bound=Callable)
605+
606+
def decorator(func: _F) -> _F:
629607
func._override_animation = animation_class
630608
return func
631609

manim/animation/protocol.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Protocol, Sequence
4+
5+
if TYPE_CHECKING:
6+
from .animation import Animation
7+
from .scene_buffer import SceneBuffer
8+
9+
10+
__all__ = (
11+
"AnimationProtocol",
12+
)
13+
14+
15+
class AnimationProtocol(Protocol):
16+
buffer: SceneBuffer
17+
18+
def begin(self) -> None:
19+
...
20+
21+
def finish(self) -> None:
22+
...
23+
24+
def get_all_animations(self) -> Sequence[Animation]:
25+
...
26+
27+
def update_mobjects(self, dt: float) -> None:
28+
...
29+
30+
def interpolate(self, alpha: float) -> None:
31+
...
32+
33+
def get_run_time(self) -> float:
34+
...

manim/animation/scene_buffer.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import TYPE_CHECKING, final, Sequence
2+
3+
if TYPE_CHECKING:
4+
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
5+
6+
7+
__all__ = ["SceneBuffer"]
8+
9+
10+
@final
11+
class SceneBuffer:
12+
"""
13+
A "buffer" between :class:`.Scene` and :class:`.Animation`
14+
15+
Operations an animation wants to do on :class:`.Scene` should be
16+
done here (eg. :meth:`.Scene.add`, :meth:`.Scene.remove`). The
17+
scene will then apply these changes at specific points (namely
18+
at the beginning and end of animations)
19+
20+
It is the scenes job to clear the buffer in between the beginning
21+
and end of animations.
22+
"""
23+
24+
def __init__(self) -> None:
25+
self.to_remove: list[Mobject] = []
26+
self.to_add: list[Mobject] = []
27+
self.deferred = False
28+
29+
def add(self, *mobs: Mobject) -> None:
30+
check_args(mobs)
31+
self._check_deferred()
32+
self.to_add.extend(mobs)
33+
34+
def remove(self, *mobs: Mobject) -> None:
35+
check_args(mobs)
36+
self._check_deferred()
37+
self.to_remove.extend(mobs)
38+
39+
def clear(self) -> None:
40+
self.to_remove.clear()
41+
self.to_add.clear()
42+
43+
def deferred_clear(self) -> None:
44+
"""Clear ``self`` on next operation"""
45+
self.deferred = True
46+
47+
def _check_deferred(self) -> None:
48+
if self.deferred:
49+
self.clear()
50+
self.deferred = False
51+
52+
def __str__(self) -> str:
53+
to_add = self.to_add
54+
to_remove = self.to_remove
55+
return f"{type(self).__name__}({to_add=}, {to_remove=})"
56+
57+
__repr__ = __str__
58+
59+
60+
def check_args(mobs: Sequence[Mobject]) -> None:
61+
if not (
62+
isinstance(mobs, Sequence)
63+
and all(isinstance(x, Mobject) for x in mobs)
64+
):
65+
raise TypeError("Must set to a list of Mobjects")

0 commit comments

Comments
 (0)