mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
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:
parent
b605d2ed84
commit
8852ceee6e
7 changed files with 189 additions and 33 deletions
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue