Skip to content

Commit 1ce3edd

Browse files
chopan050behackl
andauthored
AnimationGroup: optimized interpolate() and fixed alpha bug on finish() (#3542)
* Optimized AnimationGroup computation of start-end times with lag ratio * Added extra comment for init_run_time * Added full path to imports in composition.py * Optimized AnimationGroup.interpolate * Fixed final bugs * Removed accidental print * Final fix to AnimationGroup.interpolate * Fixed animations being skipped unintentionally * Addressed requested changes --------- Co-authored-by: Benjamin Hackl <[email protected]>
1 parent 98641a2 commit 1ce3edd

File tree

4 files changed

+65
-38
lines changed

4 files changed

+65
-38
lines changed

manim/animation/animation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ def set_run_time(self, run_time: float) -> Animation:
404404
self.run_time = run_time
405405
return self
406406

407+
# TODO: is this getter even necessary?
407408
def get_run_time(self) -> float:
408409
"""Get the run time of the animation.
409410

manim/animation/composition.py

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,19 @@
77

88
import numpy as np
99

10+
from manim._config import config
11+
from manim.animation.animation import Animation, prepare_animation
12+
from manim.constants import RendererType
13+
from manim.mobject.mobject import Group, Mobject
1014
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
15+
from manim.scene.scene import Scene
16+
from manim.utils.iterables import remove_list_redundancies
1117
from manim.utils.parameter_parsing import flatten_iterable_parameters
12-
13-
from .._config import config
14-
from ..animation.animation import Animation, prepare_animation
15-
from ..constants import RendererType
16-
from ..mobject.mobject import Group, Mobject
17-
from ..scene.scene import Scene
18-
from ..utils.iterables import remove_list_redundancies
19-
from ..utils.rate_functions import linear
18+
from manim.utils.rate_functions import linear
2019

2120
if TYPE_CHECKING:
2221
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
23-
24-
from ..mobject.types.vectorized_mobject import VGroup
22+
from manim.mobject.types.vectorized_mobject import VGroup
2523

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

@@ -93,6 +91,7 @@ def begin(self) -> None:
9391
f"{self} has a run_time of 0 seconds, this cannot be "
9492
f"rendered correctly. {tmp}."
9593
)
94+
self.anim_group_time = 0.0
9695
if self.suspend_mobject_updating:
9796
self.group.suspend_updating()
9897
for anim in self.animations:
@@ -103,8 +102,9 @@ def _setup_scene(self, scene) -> None:
103102
anim._setup_scene(scene)
104103

105104
def finish(self) -> None:
106-
for anim in self.animations:
107-
anim.finish()
105+
self.interpolate(1)
106+
self.anims_begun[:] = True
107+
self.anims_finished[:] = True
108108
if self.suspend_mobject_updating:
109109
self.group.resume_updating()
110110

@@ -116,7 +116,9 @@ def clean_up_from_scene(self, scene: Scene) -> None:
116116
anim.clean_up_from_scene(scene)
117117

118118
def update_mobjects(self, dt: float) -> None:
119-
for anim in self.animations:
119+
for anim in self.anims_with_timings["anim"][
120+
self.anims_begun & ~self.anims_finished
121+
]:
120122
anim.update_mobjects(dt)
121123

122124
def init_run_time(self, run_time) -> float:
@@ -133,37 +135,61 @@ def init_run_time(self, run_time) -> float:
133135
The duration of the animation in seconds.
134136
"""
135137
self.build_animations_with_timings()
136-
if self.anims_with_timings:
137-
self.max_end_time = np.max([awt[2] for awt in self.anims_with_timings])
138-
else:
139-
self.max_end_time = 0
138+
# Note: if lag_ratio < 1, then not necessarily the final animation's
139+
# end time will be the max end time! Therefore we must calculate the
140+
# maximum over all the end times, and not just take the last one.
141+
# Example: if you want to play 2 animations of 10s and 1s with a
142+
# lag_ratio of 0.1, the 1st one will end at t=10 and the 2nd one will
143+
# end at t=2, so the AnimationGroup will end at t=10.
144+
self.max_end_time = max(self.anims_with_timings["end"], default=0)
140145
return self.max_end_time if run_time is None else run_time
141146

142147
def build_animations_with_timings(self) -> None:
143148
"""Creates a list of triplets of the form (anim, start_time, end_time)."""
144-
self.anims_with_timings = []
145-
curr_time: float = 0
146-
for anim in self.animations:
147-
start_time: float = curr_time
148-
end_time: float = start_time + anim.get_run_time()
149-
self.anims_with_timings.append((anim, start_time, end_time))
150-
# Start time of next animation is based on the lag_ratio
151-
curr_time = (1 - self.lag_ratio) * start_time + self.lag_ratio * end_time
149+
run_times = np.array([anim.run_time for anim in self.animations])
150+
num_animations = run_times.shape[0]
151+
dtype = [("anim", "O"), ("start", "f8"), ("end", "f8")]
152+
self.anims_with_timings = np.zeros(num_animations, dtype=dtype)
153+
self.anims_begun = np.zeros(num_animations, dtype=bool)
154+
self.anims_finished = np.zeros(num_animations, dtype=bool)
155+
if num_animations == 0:
156+
return
157+
158+
lags = run_times[:-1] * self.lag_ratio
159+
self.anims_with_timings["anim"] = self.animations
160+
self.anims_with_timings["start"][1:] = np.add.accumulate(lags)
161+
self.anims_with_timings["end"] = self.anims_with_timings["start"] + run_times
152162

153163
def interpolate(self, alpha: float) -> None:
154164
# Note, if the run_time of AnimationGroup has been
155165
# set to something other than its default, these
156166
# times might not correspond to actual times,
157167
# e.g. of the surrounding scene. Instead they'd
158168
# be a rescaled version. But that's okay!
159-
time = self.rate_func(alpha) * self.max_end_time
160-
for anim, start_time, end_time in self.anims_with_timings:
161-
anim_time = end_time - start_time
162-
if anim_time == 0:
163-
sub_alpha = 0
164-
else:
165-
sub_alpha = np.clip((time - start_time) / anim_time, 0, 1)
166-
anim.interpolate(sub_alpha)
169+
anim_group_time = self.rate_func(alpha) * self.max_end_time
170+
time_goes_back = anim_group_time < self.anim_group_time
171+
172+
# Only update ongoing animations
173+
awt = self.anims_with_timings
174+
new_begun = anim_group_time >= awt["start"]
175+
new_finished = anim_group_time > awt["end"]
176+
to_update = awt[
177+
(self.anims_begun | new_begun) & (~self.anims_finished | ~new_finished)
178+
]
179+
180+
run_times = to_update["end"] - to_update["start"]
181+
sub_alphas = (anim_group_time - to_update["start"]) / run_times
182+
if time_goes_back:
183+
sub_alphas[sub_alphas < 0] = 0
184+
else:
185+
sub_alphas[sub_alphas > 1] = 1
186+
187+
for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
188+
anim_to_update.interpolate(sub_alpha)
189+
190+
self.anim_group_time = anim_group_time
191+
self.anims_begun = new_begun
192+
self.anims_finished = new_finished
167193

168194

169195
class Succession(AnimationGroup):
@@ -238,8 +264,8 @@ def update_active_animation(self, index: int) -> None:
238264
self.active_animation = self.animations[index]
239265
self.active_animation._setup_scene(self.scene)
240266
self.active_animation.begin()
241-
self.active_start_time = self.anims_with_timings[index][1]
242-
self.active_end_time = self.anims_with_timings[index][2]
267+
self.active_start_time = self.anims_with_timings[index]["start"]
268+
self.active_end_time = self.anims_with_timings[index]["end"]
243269

244270
def next_animation(self) -> None:
245271
"""Proceeds to the next animation.
@@ -256,7 +282,7 @@ def interpolate(self, alpha: float) -> None:
256282
self.next_animation()
257283
if self.active_animation is not None and self.active_start_time is not None:
258284
elapsed = current_time - self.active_start_time
259-
active_run_time = self.active_animation.get_run_time()
285+
active_run_time = self.active_animation.run_time
260286
subalpha = elapsed / active_run_time if active_run_time != 0.0 else 1.0
261287
self.active_animation.interpolate(subalpha)
262288

tests/module/animation/test_composition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def test_animationgroup_with_wait():
128128
animation_group.begin()
129129
timings = animation_group.anims_with_timings
130130

131-
assert timings == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)]
131+
assert timings.tolist() == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)]
132132

133133

134134
@pytest.mark.parametrize(

tests/opengl/test_composition_opengl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,4 @@ def test_animationgroup_with_wait(using_opengl_renderer):
104104
animation_group.begin()
105105
timings = animation_group.anims_with_timings
106106

107-
assert timings == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)]
107+
assert timings.tolist() == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)]

0 commit comments

Comments
 (0)