Skip to content

Commit f81d571

Browse files
chopan050behackl
andauthored
Allow animations with run_time=0 and implement convenience Add animation (#4017)
* Allow animations with run_time=0 and implement convenience Add animation * Modify examples so that there are less characters per line * Docstring fixes * Update manim/animation/animation.py * Address comments --------- Co-authored-by: Benjamin Hackl <[email protected]>
1 parent 862504f commit f81d571

File tree

4 files changed

+177
-50
lines changed

4 files changed

+177
-50
lines changed

manim/animation/animation.py

Lines changed: 101 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77
from .. import config, logger
88
from ..constants import RendererType
99
from ..mobject import mobject
10-
from ..mobject.mobject import Mobject
10+
from ..mobject.mobject import Group, Mobject
1111
from ..mobject.opengl import opengl_mobject
1212
from ..utils.rate_functions import linear, smooth
1313

14-
__all__ = ["Animation", "Wait", "override_animation"]
14+
__all__ = ["Animation", "Wait", "Add", "override_animation"]
1515

1616

1717
from collections.abc import Iterable, Sequence
1818
from copy import deepcopy
1919
from functools import partialmethod
20-
from typing import TYPE_CHECKING, Callable
20+
from typing import TYPE_CHECKING, Any, Callable
2121

2222
from typing_extensions import Self
2323

@@ -172,6 +172,19 @@ def __init__(
172172
),
173173
)
174174

175+
@property
176+
def run_time(self) -> float:
177+
return self._run_time
178+
179+
@run_time.setter
180+
def run_time(self, value: float) -> None:
181+
if value < 0:
182+
raise ValueError(
183+
f"The run_time of {self.__class__.__name__} cannot be "
184+
f"negative. The given value was {value}."
185+
)
186+
self._run_time = value
187+
175188
def _typecheck_input(self, mobject: Mobject | None) -> None:
176189
if mobject is None:
177190
logger.debug("Animation with empty mobject")
@@ -194,7 +207,6 @@ def begin(self) -> None:
194207
method.
195208
196209
"""
197-
self.run_time = validate_run_time(self.run_time, str(self))
198210
self.starting_mobject = self.create_starting_mobject()
199211
if self.suspend_mobject_updating:
200212
# All calls to self.mobject's internal updaters
@@ -569,33 +581,6 @@ def prepare_animation(
569581
raise TypeError(f"Object {anim} cannot be converted to an animation")
570582

571583

572-
def validate_run_time(
573-
run_time: float, caller_name: str, parameter_name: str = "run_time"
574-
) -> float:
575-
if run_time <= 0:
576-
raise ValueError(
577-
f"{caller_name} has a {parameter_name} of {run_time:g} <= 0 "
578-
f"seconds which Manim cannot render. Please set the "
579-
f"{parameter_name} to a positive number."
580-
)
581-
582-
# config.frame_rate holds the number of frames per second
583-
fps = config.frame_rate
584-
seconds_per_frame = 1 / fps
585-
if run_time < seconds_per_frame:
586-
logger.warning(
587-
f"The original {parameter_name} of {caller_name}, {run_time:g} "
588-
f"seconds, is too short for the current frame rate of {fps:g} "
589-
f"FPS. Rendering with the shortest possible {parameter_name} of "
590-
f"{seconds_per_frame:g} seconds instead."
591-
)
592-
new_run_time = seconds_per_frame
593-
else:
594-
new_run_time = run_time
595-
596-
return new_run_time
597-
598-
599584
class Wait(Animation):
600585
"""A "no operation" animation.
601586
@@ -638,7 +623,91 @@ def __init__(
638623
self.mobject.shader_wrapper_list = []
639624

640625
def begin(self) -> None:
641-
self.run_time = validate_run_time(self.run_time, str(self))
626+
pass
627+
628+
def finish(self) -> None:
629+
pass
630+
631+
def clean_up_from_scene(self, scene: Scene) -> None:
632+
pass
633+
634+
def update_mobjects(self, dt: float) -> None:
635+
pass
636+
637+
def interpolate(self, alpha: float) -> None:
638+
pass
639+
640+
641+
class Add(Animation):
642+
"""Add Mobjects to a scene, without animating them in any other way. This
643+
is similar to the :meth:`.Scene.add` method, but :class:`Add` is an
644+
animation which can be grouped into other animations.
645+
646+
Parameters
647+
----------
648+
mobjects
649+
One :class:`~.Mobject` or more to add to a scene.
650+
run_time
651+
The duration of the animation after adding the ``mobjects``. Defaults
652+
to 0, which means this is an instant animation without extra wait time
653+
after adding them.
654+
**kwargs
655+
Additional arguments to pass to the parent :class:`Animation` class.
656+
657+
Examples
658+
--------
659+
660+
.. manim:: DefaultAddScene
661+
662+
class DefaultAddScene(Scene):
663+
def construct(self):
664+
text_1 = Text("I was added with Add!")
665+
text_2 = Text("Me too!")
666+
text_3 = Text("And me!")
667+
texts = VGroup(text_1, text_2, text_3).arrange(DOWN)
668+
rect = SurroundingRectangle(texts, buff=0.5)
669+
670+
self.play(
671+
Create(rect, run_time=3.0),
672+
Succession(
673+
Wait(1.0),
674+
# You can Add a Mobject in the middle of an animation...
675+
Add(text_1),
676+
Wait(1.0),
677+
# ...or multiple Mobjects at once!
678+
Add(text_2, text_3),
679+
),
680+
)
681+
self.wait()
682+
683+
.. manim:: AddWithRunTimeScene
684+
685+
class AddWithRunTimeScene(Scene):
686+
def construct(self):
687+
# A 5x5 grid of circles
688+
circles = VGroup(
689+
*[Circle(radius=0.5) for _ in range(25)]
690+
).arrange_in_grid(5, 5)
691+
692+
self.play(
693+
Succession(
694+
# Add a run_time of 0.2 to wait for 0.2 seconds after
695+
# adding the circle, instead of using Wait(0.2) after Add!
696+
*[Add(circle, run_time=0.2) for circle in circles],
697+
rate_func=smooth,
698+
)
699+
)
700+
self.wait()
701+
"""
702+
703+
def __init__(
704+
self, *mobjects: Mobject, run_time: float = 0.0, **kwargs: Any
705+
) -> None:
706+
mobject = mobjects[0] if len(mobjects) == 1 else Group(*mobjects)
707+
super().__init__(mobject, run_time=run_time, introducer=True, **kwargs)
708+
709+
def begin(self) -> None:
710+
pass
642711

643712
def finish(self) -> None:
644713
pass

manim/animation/composition.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import numpy as np
1010

1111
from manim._config import config
12-
from manim.animation.animation import Animation, prepare_animation, validate_run_time
12+
from manim.animation.animation import Animation, prepare_animation
1313
from manim.constants import RendererType
1414
from manim.mobject.mobject import Group, Mobject
1515
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
@@ -87,7 +87,6 @@ def begin(self) -> None:
8787
f"Trying to play {self} without animations, this is not supported. "
8888
"Please add at least one subanimation."
8989
)
90-
self.run_time = validate_run_time(self.run_time, str(self))
9190
self.anim_group_time = 0.0
9291
if self.suspend_mobject_updating:
9392
self.group.suspend_updating()
@@ -175,11 +174,13 @@ def interpolate(self, alpha: float) -> None:
175174
]
176175

177176
run_times = to_update["end"] - to_update["start"]
177+
with_zero_run_time = run_times == 0
178+
run_times[with_zero_run_time] = 1
178179
sub_alphas = (anim_group_time - to_update["start"]) / run_times
179180
if time_goes_back:
180-
sub_alphas[sub_alphas < 0] = 0
181+
sub_alphas[(sub_alphas < 0) | with_zero_run_time] = 0
181182
else:
182-
sub_alphas[sub_alphas > 1] = 1
183+
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
183184

184185
for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
185186
anim_to_update.interpolate(sub_alpha)
@@ -235,7 +236,6 @@ def begin(self) -> None:
235236
f"Trying to play {self} without animations, this is not supported. "
236237
"Please add at least one subanimation."
237238
)
238-
self.run_time = validate_run_time(self.run_time, str(self))
239239
self.update_active_animation(0)
240240

241241
def finish(self) -> None:

manim/scene/scene.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from manim.mobject.opengl.opengl_mobject import OpenGLPoint
3838

3939
from .. import config, logger
40-
from ..animation.animation import Animation, Wait, prepare_animation, validate_run_time
40+
from ..animation.animation import Animation, Wait, prepare_animation
4141
from ..camera.camera import Camera
4242
from ..constants import *
4343
from ..gui.gui import configure_pygui
@@ -1020,6 +1020,35 @@ def get_time_progression(
10201020
)
10211021
return time_progression
10221022

1023+
@classmethod
1024+
def validate_run_time(
1025+
cls,
1026+
run_time: float,
1027+
method: Callable[[Any, ...], Any],
1028+
parameter_name: str = "run_time",
1029+
) -> float:
1030+
method_name = f"{cls.__name__}.{method.__name__}()"
1031+
if run_time <= 0:
1032+
raise ValueError(
1033+
f"{method_name} has a {parameter_name} of "
1034+
f"{run_time:g} <= 0 seconds which Manim cannot render. "
1035+
f"The {parameter_name} must be a positive number."
1036+
)
1037+
1038+
# config.frame_rate holds the number of frames per second
1039+
fps = config.frame_rate
1040+
seconds_per_frame = 1 / fps
1041+
if run_time < seconds_per_frame:
1042+
logger.warning(
1043+
f"The original {parameter_name} of {method_name}, "
1044+
f"{run_time:g} seconds, is too short for the current frame "
1045+
f"rate of {fps:g} FPS. Rendering with the shortest possible "
1046+
f"{parameter_name} of {seconds_per_frame:g} seconds instead."
1047+
)
1048+
run_time = seconds_per_frame
1049+
1050+
return run_time
1051+
10231052
def get_run_time(self, animations: list[Animation]):
10241053
"""
10251054
Gets the total run time for a list of animations.
@@ -1035,7 +1064,9 @@ def get_run_time(self, animations: list[Animation]):
10351064
float
10361065
The total ``run_time`` of all of the animations in the list.
10371066
"""
1038-
return max(animation.run_time for animation in animations)
1067+
run_time = max(animation.run_time for animation in animations)
1068+
run_time = self.validate_run_time(run_time, self.play, "total run_time")
1069+
return run_time
10391070

10401071
def play(
10411072
self,
@@ -1131,7 +1162,7 @@ def wait(
11311162
--------
11321163
:class:`.Wait`, :meth:`.should_mobjects_update`
11331164
"""
1134-
duration = validate_run_time(duration, str(self) + ".wait()", "duration")
1165+
duration = self.validate_run_time(duration, self.wait, "duration")
11351166
self.play(
11361167
Wait(
11371168
run_time=duration,
@@ -1155,7 +1186,7 @@ def pause(self, duration: float = DEFAULT_WAIT_TIME):
11551186
--------
11561187
:meth:`.wait`, :class:`.Wait`
11571188
"""
1158-
duration = validate_run_time(duration, str(self) + ".pause()", "duration")
1189+
duration = self.validate_run_time(duration, self.pause, "duration")
11591190
self.wait(duration=duration, frozen_frame=True)
11601191

11611192
def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
@@ -1169,7 +1200,7 @@ def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
11691200
max_time
11701201
The maximum wait time in seconds.
11711202
"""
1172-
max_time = validate_run_time(max_time, str(self) + ".wait_until()", "max_time")
1203+
max_time = self.validate_run_time(max_time, self.wait_until, "max_time")
11731204
self.wait(max_time, stop_condition=stop_condition)
11741205

11751206
def compile_animation_data(

tests/module/animation/test_animation.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55
from manim import FadeIn, Scene
66

77

8-
@pytest.mark.parametrize(
9-
"run_time",
10-
[0, -1],
11-
)
12-
def test_animation_forbidden_run_time(run_time):
8+
def test_animation_zero_total_run_time():
139
test_scene = Scene()
1410
with pytest.raises(
15-
ValueError, match="Please set the run_time to a positive number."
11+
ValueError, match="The total run_time must be a positive number."
1612
):
17-
test_scene.play(FadeIn(None, run_time=run_time))
13+
test_scene.play(FadeIn(None, run_time=0))
14+
15+
16+
def test_single_animation_zero_run_time_with_more_animations():
17+
test_scene = Scene()
18+
test_scene.play(FadeIn(None, run_time=0), FadeIn(None, run_time=1))
19+
20+
21+
def test_animation_negative_run_time():
22+
with pytest.raises(ValueError, match="The run_time of FadeIn cannot be negative."):
23+
FadeIn(None, run_time=-1)
1824

1925

2026
def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config):
@@ -23,8 +29,29 @@ def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config):
2329
assert "too short for the current frame rate" in manim_caplog.text
2430

2531

32+
@pytest.mark.parametrize("duration", [0, -1])
33+
def test_wait_invalid_duration(duration):
34+
test_scene = Scene()
35+
with pytest.raises(ValueError, match="The duration must be a positive number."):
36+
test_scene.wait(duration)
37+
38+
2639
@pytest.mark.parametrize("frozen_frame", [False, True])
27-
def test_wait_run_time_shorter_than_frame_rate(manim_caplog, frozen_frame):
40+
def test_wait_duration_shorter_than_frame_rate(manim_caplog, frozen_frame):
2841
test_scene = Scene()
2942
test_scene.wait(1e-9, frozen_frame=frozen_frame)
3043
assert "too short for the current frame rate" in manim_caplog.text
44+
45+
46+
@pytest.mark.parametrize("duration", [0, -1])
47+
def test_pause_invalid_duration(duration):
48+
test_scene = Scene()
49+
with pytest.raises(ValueError, match="The duration must be a positive number."):
50+
test_scene.pause(duration)
51+
52+
53+
@pytest.mark.parametrize("max_time", [0, -1])
54+
def test_wait_until_invalid_max_time(max_time):
55+
test_scene = Scene()
56+
with pytest.raises(ValueError, match="The max_time must be a positive number."):
57+
test_scene.wait_until(lambda: True, max_time)

0 commit comments

Comments
 (0)