Skip to content
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

AnimationGroup: optimized interpolate() and fixed alpha bug on finish() #3542

Merged
merged 11 commits into from
Apr 27, 2024
1 change: 1 addition & 0 deletions manim/animation/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ def set_run_time(self, run_time: float) -> Animation:
self.run_time = run_time
return self

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

Expand Down
96 changes: 60 additions & 36 deletions manim/animation/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@

import numpy as np

from manim._config import config
from manim.animation.animation import Animation, prepare_animation
from manim.constants import RendererType
from manim.mobject.mobject import Group, Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
from manim.scene.scene import Scene
from manim.utils.iterables import remove_list_redundancies
from manim.utils.parameter_parsing import flatten_iterable_parameters

from .._config import config
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
from manim.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 manim.mobject.types.vectorized_mobject import VGroup

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

Expand Down Expand Up @@ -94,6 +92,7 @@ def begin(self) -> None:
f"{self} has a run_time of 0 seconds, this cannot be "
f"rendered correctly. {tmp}."
)
self.anim_group_time = 0.0
if self.suspend_mobject_updating:
self.group.suspend_updating()
for anim in self.animations:
Expand All @@ -104,8 +103,9 @@ def _setup_scene(self, scene) -> None:
anim._setup_scene(scene)

def finish(self) -> None:
for anim in self.animations:
anim.finish()
self.interpolate(1)
self.anims_begun[:] = False
self.anims_finished[:] = False
chopan050 marked this conversation as resolved.
Show resolved Hide resolved
if self.suspend_mobject_updating:
self.group.resume_updating()

Expand All @@ -117,7 +117,9 @@ def clean_up_from_scene(self, scene: Scene) -> None:
anim.clean_up_from_scene(scene)

def update_mobjects(self, dt: float) -> None:
for anim in self.animations:
for anim in self.anims_with_timings["anim"][
self.anims_begun & ~self.anims_finished
]:
anim.update_mobjects(dt)

def init_run_time(self, run_time) -> float:
Expand All @@ -134,37 +136,59 @@ def init_run_time(self, run_time) -> float:
The duration of the animation in seconds.
"""
self.build_animations_with_timings()
if self.anims_with_timings:
self.max_end_time = np.max([awt[2] for awt in self.anims_with_timings])
else:
self.max_end_time = 0
# Note: if lag_ratio < 1, then not necessarily the final animation's
# end time will be the max end time!
chopan050 marked this conversation as resolved.
Show resolved Hide resolved
self.max_end_time = max(self.anims_with_timings["end"], default=0)
return self.max_end_time if run_time is None else run_time

def build_animations_with_timings(self) -> None:
"""Creates a list of triplets of the form (anim, start_time, end_time)."""
self.anims_with_timings = []
curr_time: float = 0
for anim in self.animations:
start_time: float = curr_time
end_time: float = start_time + anim.get_run_time()
self.anims_with_timings.append((anim, start_time, end_time))
# Start time of next animation is based on the lag_ratio
curr_time = (1 - self.lag_ratio) * start_time + self.lag_ratio * end_time
run_times = np.array([anim.run_time for anim in self.animations])
num_animations = run_times.shape[0]
dtype = [("anim", "O"), ("start", "f8"), ("end", "f8")]
self.anims_with_timings = np.zeros(num_animations, dtype=dtype)
self.anims_begun = np.zeros(num_animations, dtype=bool)
self.anims_finished = np.zeros(num_animations, dtype=bool)
if num_animations == 0:
return

lags = run_times[:-1] * self.lag_ratio
self.anims_with_timings["anim"] = self.animations
self.anims_with_timings["start"][1:] = np.add.accumulate(lags)
self.anims_with_timings["end"] = self.anims_with_timings["start"] + run_times

def interpolate(self, alpha: float) -> None:
# Note, if the run_time of AnimationGroup has been
# set to something other than its default, these
# times might not correspond to actual times,
# e.g. of the surrounding scene. Instead they'd
# be a rescaled version. But that's okay!
time = self.rate_func(alpha) * self.max_end_time
for anim, start_time, end_time in self.anims_with_timings:
anim_time = end_time - start_time
if anim_time == 0:
sub_alpha = 0
else:
sub_alpha = np.clip((time - start_time) / anim_time, 0, 1)
anim.interpolate(sub_alpha)
anim_group_time = self.rate_func(alpha) * self.max_end_time
time_goes_back = anim_group_time < self.anim_group_time

# Only update ongoing animations
awt = self.anims_with_timings
new_begun = anim_group_time >= awt["start"]
new_finished = anim_group_time > awt["end"]
to_update = awt[
(self.anims_begun | new_begun) & (~self.anims_finished | ~new_finished)
]

run_times = to_update["end"] - to_update["start"]
null = run_times == 0.0
chopan050 marked this conversation as resolved.
Show resolved Hide resolved
sub_alphas = anim_group_time - to_update["start"]
sub_alphas[~null] /= run_times[~null]
if time_goes_back:
sub_alphas[null | (sub_alphas < 0)] = 0
else:
sub_alphas[null | (sub_alphas > 1)] = 1

for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
anim_to_update.interpolate(sub_alpha)

self.anim_group_time = anim_group_time
self.anims_begun = new_begun
self.anims_finished = new_finished


class Succession(AnimationGroup):
Expand Down Expand Up @@ -239,8 +263,8 @@ def update_active_animation(self, index: int) -> None:
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]
self.active_start_time = self.anims_with_timings[index]["start"]
self.active_end_time = self.anims_with_timings[index]["end"]

def next_animation(self) -> None:
"""Proceeds to the next animation.
Expand All @@ -257,7 +281,7 @@ def interpolate(self, alpha: float) -> None:
self.next_animation()
if self.active_animation is not None and self.active_start_time is not None:
elapsed = current_time - self.active_start_time
active_run_time = self.active_animation.get_run_time()
active_run_time = self.active_animation.run_time
subalpha = elapsed / active_run_time if active_run_time != 0.0 else 1.0
self.active_animation.interpolate(subalpha)

Expand Down
2 changes: 1 addition & 1 deletion tests/module/animation/test_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def test_animationgroup_with_wait():
animation_group.begin()
timings = animation_group.anims_with_timings

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


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion tests/opengl/test_composition_opengl.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ def test_animationgroup_with_wait(using_opengl_renderer):
animation_group.begin()
timings = animation_group.anims_with_timings

assert timings == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)]
assert timings.tolist() == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)]
Loading