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

Allow playing Animations asynchronously using OpenGLRenderer #1528

Closed
wants to merge 10 commits into from
33 changes: 0 additions & 33 deletions manim/mobject/mobject_update_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"always_redraw",
"always_shift",
"always_rotate",
"turn_animation_into_updater",
"cycle_animation",
]

Expand Down Expand Up @@ -74,37 +73,5 @@ def always_rotate(mobject, rate=20 * DEGREES, **kwargs):
return mobject


def turn_animation_into_updater(animation, cycle=False, **kwargs):
"""
Add an updater to the animation's mobject which applies
the interpolation and update functions of the animation

If cycle is True, this repeats over and over. Otherwise,
the updater will be popped upon completion
"""
mobject = animation.mobject
animation.suspend_mobject_updating = False
animation.begin()
animation.total_time = 0

def update(m, dt):
run_time = animation.get_run_time()
time_ratio = animation.total_time / run_time
if cycle:
alpha = time_ratio % 1
else:
alpha = np.clip(time_ratio, 0, 1)
if alpha >= 1:
animation.finish()
m.remove_updater(update)
return
animation.interpolate(alpha)
animation.update_mobjects(dt)
animation.total_time += dt

mobject.add_updater(update)
return mobject


def cycle_animation(animation, **kwargs):
return turn_animation_into_updater(animation, cycle=True, **kwargs)
92 changes: 90 additions & 2 deletions manim/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import types
import warnings
from queue import Queue
from typing import Any, Optional

import numpy as np
from tqdm import tqdm
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(
self.random_seed = random_seed

self.animations = None
self.sync_play_running = False
self.stop_condition = None
self.moving_mobjects = None
self.static_mobjects = None
Expand Down Expand Up @@ -849,8 +851,92 @@ def get_run_time(self, animations):
else:
return np.max([animation.run_time for animation in animations])

def play(self, *args, **kwargs):
self.renderer.play(self, *args, **kwargs)
def play(
self, *animations: Animation, run_async: Optional[bool] = None, **kwargs: Any
):
"""Play :class:`Animations<.Animation>` in the scene.

Parameters
----------
animations
The animations to be played. They are started at the same time and played
in parallel.
run_async
Whether to play the animations asynchronously. Asynchronous animations don't
interrupt the execution of :meth:`construct` and can be started while
synchronous animations are running. If ``run_async`` is not defined, it
defaults to ``True`` if called while a synchronous animation is running,
to ``False`` otherwise.

.. note::

This feature is only available when using the OpenGL-Renderer. The
parameter will be ignored otherwise.
kwargs
Attributes of all passed animations to be set. Typical usecases are setting
`run_time` or `rate_func`.

Raises
------
ValueError
If an animation is played synchronously while another synchronous animation
is running.
"""

if config["renderer"] != "opengl":
run_async = False

if self.sync_play_running:
if run_async is None:
run_async = True
if not run_async:
raise ValueError(
"Cannot start a synchronous animation while another is running."
)
elif run_async is None:
run_async = False

if run_async:
animations = self.compile_animations(*animations, **kwargs)

self.add_mobjects_from_animations(animations)
for animation in animations:
self._turn_animation_to_updater(animation)
return
Comment on lines +889 to +905
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the whole logic can be simplified.
You could do instead

<run_sync is False by default)
if self.sync_play_running 
   run_sync = False if config[blabla] = blab else run_sync
   if not run_sync: 
       # ValueError blabla
   # and then the play logic (inside l 899). 
# here (outside the if) the normal play logic. 

What do you think ?

else:
self.renderer.play(self, *animations, **kwargs)

def _turn_animation_to_updater(self, animation: Animation):
"""Turn an :class:`~.Animation` into an updater of it's :class:`~.Mobject`.

The updater will remove itself when the animation is over.

Parameters
----------
animation
The animation to be added

"""
mobject = animation.mobject

animation.suspend_mobject_updating = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question : why ?

animation.begin()

def update(mob, dt):
run_time = animation.get_run_time()
alpha = update.total_time / run_time
if alpha >= 1:
animation.finish()
animation.clean_up_from_scene(self)
mob.remove_updater(update)
return
animation.interpolate(alpha)
animation.update_mobjects(dt)
update.total_time += dt

update.total_time = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coment : Although I undestand what you're doing here, I'm not really a big fan of adding attributes to functions.
Why not turing update into a proper class with a __call__ method ?

mobject.add_updater(update)
return self

def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
self.play(Wait(run_time=duration, stop_condition=stop_condition))
Expand Down Expand Up @@ -946,6 +1032,7 @@ def play_internal(self, skip_rendering=False):
named parameters affecting what was passed in ``args``,
e.g. ``run_time``, ``lag_ratio`` and so on.
"""
self.sync_play_running = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using a decorator here would make sense and would improve readability imo

self.duration = self.get_run_time(self.animations)
self.time_progression = self._get_animation_time_progression(
self.animations, self.duration
Expand All @@ -966,6 +1053,7 @@ def play_internal(self, skip_rendering=False):
self.renderer.static_image = None
# Closing the progress bar at the end of the play.
self.time_progression.close()
self.sync_play_running = False

def interactive_embed(self):
"""
Expand Down