Added finer control over :meth:.Scene.wait being static (i.e., no updaters) or not (#2504)

* added freeze_frame kwarg to Wait + documentation, changed logic of should_mobject_update

* changed default behavior when adding updaters to opengl mobjects

* added tests

* fixed OpenGL behavior (?)

* black

* Scene.pause, type hints, documentation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* actually handle frozen frames in OpenGL renderer

* black

* remove stray print

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Benjamin Hackl 2022-02-10 17:42:09 +01:00 committed by GitHub
commit 8852ceee6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 33 deletions

View file

@ -501,12 +501,41 @@ def prepare_animation(
class Wait(Animation):
"""A "no operation" animation.
Parameters
----------
run_time
The amount of time that should pass.
stop_condition
A function without positional arguments that evaluates to a boolean.
The function is evaluated after every new frame has been rendered.
Playing the animation only stops after the return value is truthy.
Overrides the specified ``run_time``.
frozen_frame
Controls whether or not the wait animation is static, i.e., corresponds
to a frozen frame. If ``False`` is passed, the render loop still
progresses through the animation as usual and (among other things)
continues to call updater functions. If ``None`` (the default value),
the :meth:`.Scene.play` call tries to determine whether the Wait call
can be static or not itself via :meth:`.Scene.should_mobjects_update`.
kwargs
Keyword arguments to be passed to the parent class, :class:`.Animation`.
"""
def __init__(
self, run_time: float = 1, stop_condition=None, **kwargs
): # what is stop_condition?
self,
run_time: float = 1,
stop_condition: Callable[[], bool] | None = None,
frozen_frame: bool | None = None,
**kwargs,
):
if stop_condition and frozen_frame:
raise ValueError("A static Wait animation cannot have a stop condition.")
self.duration: float = run_time
self.stop_condition = stop_condition
self.is_static_wait: bool = False
self.is_static_wait: bool = frozen_frame
super().__init__(None, run_time=run_time, **kwargs)
# quick fix to work in opengl setting:
self.mobject.shader_wrapper_list = []

View file

@ -1317,7 +1317,7 @@ class OpenGLMobject:
def get_family_updaters(self):
return list(it.chain(*(sm.get_updaters() for sm in self.get_family())))
def add_updater(self, update_function, index=None, call_updater=True):
def add_updater(self, update_function, index=None, call_updater=False):
if "dt" in get_parameters(update_function):
updater_list = self.time_based_updaters
else:

View file

@ -422,8 +422,21 @@ class OpenGLRenderer:
self.animation_start_time = time.time()
self.file_writer.begin_animation(not self.skip_animations)
if scene.compile_animation_data(*args, **kwargs):
scene.begin_animations()
scene.compile_animation_data(*args, **kwargs)
scene.begin_animations()
if scene.is_current_animation_frozen_frame():
self.update_frame(scene)
if not self.skip_animations:
for _ in range(int(config.frame_rate * scene.duration)):
self.file_writer.write_frame(self)
if self.window is not None:
while time.time() - self.animation_start_time < scene.duration:
self.window.swap_buffers()
self.animation_elapsed_time = scene.duration
else:
scene.play_internal()
self.file_writer.end_animation(not self.skip_animations)

View file

@ -13,6 +13,7 @@ import threading
import time
import types
from queue import Queue
from typing import Callable
import srt
@ -347,18 +348,33 @@ class Scene:
for func in self.updaters:
func(dt)
def should_update_mobjects(self):
def should_update_mobjects(self) -> bool:
"""
Returns True if any mobject in Scene is being updated
or if the scene has always_update_mobjects set to true.
Returns True if the mobjects of this scene should be updated.
Returns
-------
bool
In particular, this checks whether
- the :attr:`always_update_mobjects` attribute of :class:`.Scene`
is set to ``True``,
- the :class:`.Scene` itself has time-based updaters attached,
- any mobject in this :class:`.Scene` has time-based updaters attached.
This is only called when a single Wait animation is played.
"""
return self.always_update_mobjects or any(
[mob.has_time_based_updater() for mob in self.get_mobject_family_members()],
)
wait_animation = self.animations[0]
if wait_animation.is_static_wait is None:
should_update = (
self.always_update_mobjects
or self.updaters
or any(
[
mob.has_time_based_updater()
for mob in self.get_mobject_family_members()
],
)
)
wait_animation.is_static_wait = not should_update
return not wait_animation.is_static_wait
def get_top_level_mobjects(self):
"""
@ -952,8 +968,56 @@ class Scene:
offset=-run_time + subcaption_offset,
)
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
self.play(Wait(run_time=duration, stop_condition=stop_condition))
def wait(
self,
duration: float = DEFAULT_WAIT_TIME,
stop_condition: Callable[[], bool] | None = None,
frozen_frame: bool | None = None,
):
"""Plays a "no operation" animation.
Parameters
----------
duration
The run time of the animation.
stop_condition
A function without positional arguments that is evaluated every time
a frame is rendered. The animation only stops when the return value
of the function is truthy. Overrides any value passed to ``duration``.
frozen_frame
If True, updater functions are not evaluated, and the animation outputs
a frozen frame. If False, updater functions are called and frames
are rendered as usual. If None (the default), the scene tries to
determine whether or not the frame is frozen on its own.
See also
--------
:class:`.Wait`, :meth:`.should_mobjects_update`
"""
self.play(
Wait(
run_time=duration,
stop_condition=stop_condition,
frozen_frame=frozen_frame,
)
)
def pause(self, duration: float = DEFAULT_WAIT_TIME):
"""Pauses the scene (i.e., displays a frozen frame).
This is an alias for :meth:`.wait` with ``frozen_frame``
set to ``True``.
Parameters
----------
duration
The duration of the pause.
See also
--------
:meth:`.wait`, :class:`.Wait`
"""
self.wait(duration=duration, frozen_frame=True)
def wait_until(self, stop_condition, max_time=60):
"""
@ -1000,23 +1064,22 @@ class Scene:
self.moving_mobjects = []
self.static_mobjects = []
if config.renderer != "opengl":
if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
if self.should_update_mobjects():
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
self.stop_condition = self.animations[0].stop_condition
else:
self.duration = self.animations[0].duration
# Static image logic when the wait is static is done by the renderer, not here.
self.animations[0].is_static_wait = True
return None
self.stop_condition = self.animations[0].stop_condition
else:
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
(
self.moving_mobjects,
self.static_mobjects,
) = self.get_moving_and_static_mobjects(self.animations)
self.duration = self.animations[0].duration
# Static image logic when the wait is static is done by the renderer, not here.
self.animations[0].is_static_wait = True
return None
elif config.renderer != "opengl":
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
(
self.moving_mobjects,
self.static_mobjects,
) = self.get_moving_and_static_mobjects(self.animations)
self.duration = self.get_run_time(self.animations)
return self

View file

@ -8,8 +8,10 @@ from manim import *
from manim import config
from ..simple_scenes import (
SceneForFrozenFrameTests,
SceneWithMultipleCalls,
SceneWithNonStaticWait,
SceneWithSceneUpdater,
SceneWithStaticWait,
SquareToCircle,
)
@ -50,7 +52,6 @@ def test_t_values_with_skip_animations(using_temp_opengl_config, disabling_cachi
)
@pytest.mark.xfail(reason="Not currently implemented for opengl")
def test_static_wait_detection(using_temp_opengl_config, disabling_caching):
"""Test if a static wait (wait that freeze the frame) is correctly detected"""
scene = SceneWithStaticWait()
@ -65,6 +66,17 @@ def test_non_static_wait_detection(using_temp_opengl_config, disabling_caching):
scene.render()
assert not scene.animations[0].is_static_wait
assert not scene.is_current_animation_frozen_frame()
scene = SceneWithSceneUpdater()
scene.render()
assert not scene.animations[0].is_static_wait
assert not scene.is_current_animation_frozen_frame()
def test_frozen_frame(using_temp_opengl_config, disabling_caching):
scene = SceneForFrozenFrameTests()
scene.render()
assert scene.mobject_update_count == 0
assert scene.scene_update_count == 0
@pytest.mark.xfail(reason="Should be fixed in #2133")

View file

@ -43,6 +43,32 @@ class SceneWithStaticWait(Scene):
self.wait()
class SceneWithSceneUpdater(Scene):
def construct(self):
self.add(Square())
self.add_updater(lambda dt: 42)
self.wait()
class SceneForFrozenFrameTests(Scene):
def construct(self):
self.mobject_update_count = 0
self.scene_update_count = 0
def increment_mobject_update_count(mob, dt):
self.mobject_update_count += 1
def increment_scene_update_count(dt):
self.scene_update_count += 1
s = Square()
s.add_updater(increment_mobject_update_count)
self.add(s)
self.add_updater(increment_scene_update_count)
self.wait(frozen_frame=True)
class SceneWithNonStaticWait(Scene):
def construct(self):
s = Square()

View file

@ -9,8 +9,10 @@ from manim import *
from manim import config
from .simple_scenes import (
SceneForFrozenFrameTests,
SceneWithMultipleCalls,
SceneWithNonStaticWait,
SceneWithSceneUpdater,
SceneWithStaticWait,
SquareToCircle,
)
@ -65,6 +67,17 @@ def test_non_static_wait_detection(using_temp_config, disabling_caching):
scene.render()
assert not scene.animations[0].is_static_wait
assert not scene.is_current_animation_frozen_frame()
scene = SceneWithSceneUpdater()
scene.render()
assert not scene.animations[0].is_static_wait
assert not scene.is_current_animation_frozen_frame()
def test_frozen_frame(using_temp_config, disabling_caching):
scene = SceneForFrozenFrameTests()
scene.render()
assert scene.mobject_update_count == 0
assert scene.scene_update_count == 0
def test_t_values_with_cached_data(using_temp_config):