Source code for manim.animation.composition

"""Tools for displaying multiple animations at once."""


from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Sequence

import numpy as np

from manim.mobject.opengl.opengl_mobject import OpenGLGroup

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

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

    from ..mobject.types.vectorized_mobject import VGroup

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


DEFAULT_LAGGED_START_LAG_RATIO: float = 0.05


[docs]class AnimationGroup(Animation): """Plays a group or series of :class:`~.Animation`. Parameters ---------- animations Sequence of :class:`~.Animation` objects to be played. group A group of multiple :class:`~.Mobject`. run_time The duration of the animation in seconds. rate_func The function defining the animation progress based on the relative runtime (see :mod:`~.rate_functions`) . lag_ratio Defines the delay after which the animation is applied to submobjects. A lag_ratio of ``n.nn`` means the next animation will play when ``nnn%`` of the current animation has played. Defaults to 0.0, meaning that all animations will be played together. This does not influence the total runtime of the animation. Instead the runtime of individual animations is adjusted so that the complete animation has the defined run time. """ def __init__( self, *animations: Animation, group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None, run_time: float | None = None, rate_func: Callable[[float], float] = linear, lag_ratio: float = 0, **kwargs, ) -> None: self.animations = [prepare_animation(anim) for anim in animations] self.rate_func = rate_func self.group = group if self.group is None: mobjects = remove_list_redundancies( [anim.mobject for anim in self.animations if not anim.is_introducer()], ) if config["renderer"] == RendererType.OPENGL: self.group = OpenGLGroup(*mobjects) else: self.group = Group(*mobjects) super().__init__( self.group, rate_func=self.rate_func, lag_ratio=lag_ratio, **kwargs ) self.run_time: float = self.init_run_time(run_time)
[docs] def get_all_mobjects(self) -> Sequence[Mobject]: return list(self.group)
[docs] def begin(self) -> None: if self.suspend_mobject_updating: self.group.suspend_updating() for anim in self.animations: anim.begin()
[docs] def _setup_scene(self, scene) -> None: for anim in self.animations: anim._setup_scene(scene)
[docs] def finish(self) -> None: for anim in self.animations: anim.finish() if self.suspend_mobject_updating: self.group.resume_updating()
[docs] 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)
[docs] def update_mobjects(self, dt: float) -> None: for anim in self.animations: anim.update_mobjects(dt)
[docs] def init_run_time(self, run_time) -> float: """Calculates the run time of the animation, if different from ``run_time``. Parameters ---------- run_time The duration of the animation in seconds. Returns ------- run_time 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 return self.max_end_time if run_time is None else run_time
[docs] 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
[docs] 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)
[docs]class Succession(AnimationGroup): """Plays a series of animations in succession. Parameters ---------- animations Sequence of :class:`~.Animation` objects to be played. lag_ratio Defines the delay after which the animation is applied to submobjects. A lag_ratio of ``n.nn`` means the next animation will play when ``nnn%`` of the current animation has played. Defaults to 1.0, meaning that the next animation will begin when 100% of the current animation has played. This does not influence the total runtime of the animation. Instead the runtime of individual animations is adjusted so that the complete animation has the defined run time. Examples -------- .. manim:: SuccessionExample class SuccessionExample(Scene): def construct(self): dot1 = Dot(point=LEFT * 2 + UP * 2, radius=0.16, color=BLUE) dot2 = Dot(point=LEFT * 2 + DOWN * 2, radius=0.16, color=MAROON) dot3 = Dot(point=RIGHT * 2 + DOWN * 2, radius=0.16, color=GREEN) dot4 = Dot(point=RIGHT * 2 + UP * 2, radius=0.16, color=YELLOW) self.add(dot1, dot2, dot3, dot4) self.play(Succession( dot1.animate.move_to(dot2), dot2.animate.move_to(dot3), dot3.animate.move_to(dot4), dot4.animate.move_to(dot1) )) """ def __init__(self, *animations: Animation, lag_ratio: float = 1, **kwargs) -> None: super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
[docs] def begin(self) -> None: assert len(self.animations) > 0 self.update_active_animation(0)
[docs] def finish(self) -> None: while self.active_animation is not None: self.next_animation()
[docs] def update_mobjects(self, dt: float) -> None: if self.active_animation: self.active_animation.update_mobjects(dt)
[docs] 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): self.active_animation: Animation | None = None self.active_start_time: float | None = 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]
[docs] def next_animation(self) -> None: """Proceeds to the next animation. This method is called right when the active animation finishes. """ if self.active_animation is not None: self.active_animation.finish() self.update_active_animation(self.active_index + 1)
[docs] def interpolate(self, alpha: float) -> None: current_time = self.rate_func(alpha) * self.max_end_time while self.active_end_time is not None and current_time >= self.active_end_time: 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() subalpha = elapsed / active_run_time if active_run_time != 0.0 else 1.0 self.active_animation.interpolate(subalpha)
[docs]class LaggedStart(AnimationGroup): """Adjusts the timing of a series of :class:`~.Animation` according to ``lag_ratio``. Parameters ---------- animations Sequence of :class:`~.Animation` objects to be played. lag_ratio Defines the delay after which the animation is applied to submobjects. A lag_ratio of ``n.nn`` means the next animation will play when ``nnn%`` of the current animation has played. Defaults to 0.05, meaning that the next animation will begin when 5% of the current animation has played. This does not influence the total runtime of the animation. Instead the runtime of individual animations is adjusted so that the complete animation has the defined run time. Examples -------- .. manim:: LaggedStartExample class LaggedStartExample(Scene): def construct(self): title = Text("lag_ratio = 0.25").to_edge(UP) dot1 = Dot(point=LEFT * 2 + UP, radius=0.16) dot2 = Dot(point=LEFT * 2, radius=0.16) dot3 = Dot(point=LEFT * 2 + DOWN, radius=0.16) line_25 = DashedLine( start=LEFT + UP * 2, end=LEFT + DOWN * 2, color=RED ) label = Text("25%", font_size=24).next_to(line_25, UP) self.add(title, dot1, dot2, dot3, line_25, label) self.play(LaggedStart( dot1.animate.shift(RIGHT * 4), dot2.animate.shift(RIGHT * 4), dot3.animate.shift(RIGHT * 4), lag_ratio=0.25, run_time=4 )) """ def __init__( self, *animations: Animation, lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, **kwargs, ): super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
[docs]class LaggedStartMap(LaggedStart): """Plays a series of :class:`~.Animation` while mapping a function to submobjects. Parameters ---------- AnimationClass :class:`~.Animation` to apply to mobject. mobject :class:`~.Mobject` whose submobjects the animation, and optionally the function, are to be applied. arg_creator Function which will be applied to :class:`~.Mobject`. run_time The duration of the animation in seconds. Examples -------- .. manim:: LaggedStartMapExample class LaggedStartMapExample(Scene): def construct(self): title = Tex("LaggedStartMap").to_edge(UP, buff=LARGE_BUFF) dots = VGroup( *[Dot(radius=0.16) for _ in range(35)] ).arrange_in_grid(rows=5, cols=7, buff=MED_LARGE_BUFF) self.add(dots, title) # Animate yellow ripple effect for mob in dots, title: self.play(LaggedStartMap( ApplyMethod, mob, lambda m : (m.set_color, YELLOW), lag_ratio = 0.1, rate_func = there_and_back, run_time = 2 )) """ def __init__( self, AnimationClass: Callable[..., Animation], mobject: Mobject, arg_creator: Callable[[Mobject], str] = None, run_time: float = 2, **kwargs, ) -> None: args_list = [] for submob in mobject: if arg_creator: args_list.append(arg_creator(submob)) else: args_list.append((submob,)) anim_kwargs = dict(kwargs) if "lag_ratio" in anim_kwargs: anim_kwargs.pop("lag_ratio") animations = [AnimationClass(*args, **anim_kwargs) for args in args_list] super().__init__(*animations, run_time=run_time, **kwargs)