Skip to content

Commit

Permalink
Allow animations with run_time=0 and implement convenience Add an…
Browse files Browse the repository at this point in the history
…imation (ManimCommunity#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]>
  • Loading branch information
chopan050 and behackl authored Nov 27, 2024
1 parent 862504f commit f81d571
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 50 deletions.
133 changes: 101 additions & 32 deletions manim/animation/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
from .. import config, logger
from ..constants import RendererType
from ..mobject import mobject
from ..mobject.mobject import Mobject
from ..mobject.mobject import Group, Mobject
from ..mobject.opengl import opengl_mobject
from ..utils.rate_functions import linear, smooth

__all__ = ["Animation", "Wait", "override_animation"]
__all__ = ["Animation", "Wait", "Add", "override_animation"]


from collections.abc import Iterable, Sequence
from copy import deepcopy
from functools import partialmethod
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Any, Callable

from typing_extensions import Self

Expand Down Expand Up @@ -172,6 +172,19 @@ def __init__(
),
)

@property
def run_time(self) -> float:
return self._run_time

@run_time.setter
def run_time(self, value: float) -> None:
if value < 0:
raise ValueError(
f"The run_time of {self.__class__.__name__} cannot be "
f"negative. The given value was {value}."
)
self._run_time = value

def _typecheck_input(self, mobject: Mobject | None) -> None:
if mobject is None:
logger.debug("Animation with empty mobject")
Expand All @@ -194,7 +207,6 @@ def begin(self) -> None:
method.
"""
self.run_time = validate_run_time(self.run_time, str(self))
self.starting_mobject = self.create_starting_mobject()
if self.suspend_mobject_updating:
# All calls to self.mobject's internal updaters
Expand Down Expand Up @@ -569,33 +581,6 @@ def prepare_animation(
raise TypeError(f"Object {anim} cannot be converted to an animation")


def validate_run_time(
run_time: float, caller_name: str, parameter_name: str = "run_time"
) -> float:
if run_time <= 0:
raise ValueError(
f"{caller_name} has a {parameter_name} of {run_time:g} <= 0 "
f"seconds which Manim cannot render. Please set the "
f"{parameter_name} to a positive number."
)

# config.frame_rate holds the number of frames per second
fps = config.frame_rate
seconds_per_frame = 1 / fps
if run_time < seconds_per_frame:
logger.warning(
f"The original {parameter_name} of {caller_name}, {run_time:g} "
f"seconds, is too short for the current frame rate of {fps:g} "
f"FPS. Rendering with the shortest possible {parameter_name} of "
f"{seconds_per_frame:g} seconds instead."
)
new_run_time = seconds_per_frame
else:
new_run_time = run_time

return new_run_time


class Wait(Animation):
"""A "no operation" animation.
Expand Down Expand Up @@ -638,7 +623,91 @@ def __init__(
self.mobject.shader_wrapper_list = []

def begin(self) -> None:
self.run_time = validate_run_time(self.run_time, str(self))
pass

def finish(self) -> None:
pass

def clean_up_from_scene(self, scene: Scene) -> None:
pass

def update_mobjects(self, dt: float) -> None:
pass

def interpolate(self, alpha: float) -> None:
pass


class Add(Animation):
"""Add Mobjects to a scene, without animating them in any other way. This
is similar to the :meth:`.Scene.add` method, but :class:`Add` is an
animation which can be grouped into other animations.
Parameters
----------
mobjects
One :class:`~.Mobject` or more to add to a scene.
run_time
The duration of the animation after adding the ``mobjects``. Defaults
to 0, which means this is an instant animation without extra wait time
after adding them.
**kwargs
Additional arguments to pass to the parent :class:`Animation` class.
Examples
--------
.. manim:: DefaultAddScene
class DefaultAddScene(Scene):
def construct(self):
text_1 = Text("I was added with Add!")
text_2 = Text("Me too!")
text_3 = Text("And me!")
texts = VGroup(text_1, text_2, text_3).arrange(DOWN)
rect = SurroundingRectangle(texts, buff=0.5)
self.play(
Create(rect, run_time=3.0),
Succession(
Wait(1.0),
# You can Add a Mobject in the middle of an animation...
Add(text_1),
Wait(1.0),
# ...or multiple Mobjects at once!
Add(text_2, text_3),
),
)
self.wait()
.. manim:: AddWithRunTimeScene
class AddWithRunTimeScene(Scene):
def construct(self):
# A 5x5 grid of circles
circles = VGroup(
*[Circle(radius=0.5) for _ in range(25)]
).arrange_in_grid(5, 5)
self.play(
Succession(
# Add a run_time of 0.2 to wait for 0.2 seconds after
# adding the circle, instead of using Wait(0.2) after Add!
*[Add(circle, run_time=0.2) for circle in circles],
rate_func=smooth,
)
)
self.wait()
"""

def __init__(
self, *mobjects: Mobject, run_time: float = 0.0, **kwargs: Any
) -> None:
mobject = mobjects[0] if len(mobjects) == 1 else Group(*mobjects)
super().__init__(mobject, run_time=run_time, introducer=True, **kwargs)

def begin(self) -> None:
pass

def finish(self) -> None:
pass
Expand Down
10 changes: 5 additions & 5 deletions manim/animation/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import numpy as np

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

run_times = to_update["end"] - to_update["start"]
with_zero_run_time = run_times == 0
run_times[with_zero_run_time] = 1
sub_alphas = (anim_group_time - to_update["start"]) / run_times
if time_goes_back:
sub_alphas[sub_alphas < 0] = 0
sub_alphas[(sub_alphas < 0) | with_zero_run_time] = 0
else:
sub_alphas[sub_alphas > 1] = 1
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1

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

def finish(self) -> None:
Expand Down
41 changes: 36 additions & 5 deletions manim/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from manim.mobject.opengl.opengl_mobject import OpenGLPoint

from .. import config, logger
from ..animation.animation import Animation, Wait, prepare_animation, validate_run_time
from ..animation.animation import Animation, Wait, prepare_animation
from ..camera.camera import Camera
from ..constants import *
from ..gui.gui import configure_pygui
Expand Down Expand Up @@ -1020,6 +1020,35 @@ def get_time_progression(
)
return time_progression

@classmethod
def validate_run_time(
cls,
run_time: float,
method: Callable[[Any, ...], Any],
parameter_name: str = "run_time",
) -> float:
method_name = f"{cls.__name__}.{method.__name__}()"
if run_time <= 0:
raise ValueError(
f"{method_name} has a {parameter_name} of "
f"{run_time:g} <= 0 seconds which Manim cannot render. "
f"The {parameter_name} must be a positive number."
)

# config.frame_rate holds the number of frames per second
fps = config.frame_rate
seconds_per_frame = 1 / fps
if run_time < seconds_per_frame:
logger.warning(
f"The original {parameter_name} of {method_name}, "
f"{run_time:g} seconds, is too short for the current frame "
f"rate of {fps:g} FPS. Rendering with the shortest possible "
f"{parameter_name} of {seconds_per_frame:g} seconds instead."
)
run_time = seconds_per_frame

return run_time

def get_run_time(self, animations: list[Animation]):
"""
Gets the total run time for a list of animations.
Expand All @@ -1035,7 +1064,9 @@ def get_run_time(self, animations: list[Animation]):
float
The total ``run_time`` of all of the animations in the list.
"""
return max(animation.run_time for animation in animations)
run_time = max(animation.run_time for animation in animations)
run_time = self.validate_run_time(run_time, self.play, "total run_time")
return run_time

def play(
self,
Expand Down Expand Up @@ -1131,7 +1162,7 @@ def wait(
--------
:class:`.Wait`, :meth:`.should_mobjects_update`
"""
duration = validate_run_time(duration, str(self) + ".wait()", "duration")
duration = self.validate_run_time(duration, self.wait, "duration")
self.play(
Wait(
run_time=duration,
Expand All @@ -1155,7 +1186,7 @@ def pause(self, duration: float = DEFAULT_WAIT_TIME):
--------
:meth:`.wait`, :class:`.Wait`
"""
duration = validate_run_time(duration, str(self) + ".pause()", "duration")
duration = self.validate_run_time(duration, self.pause, "duration")
self.wait(duration=duration, frozen_frame=True)

def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
Expand All @@ -1169,7 +1200,7 @@ def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
max_time
The maximum wait time in seconds.
"""
max_time = validate_run_time(max_time, str(self) + ".wait_until()", "max_time")
max_time = self.validate_run_time(max_time, self.wait_until, "max_time")
self.wait(max_time, stop_condition=stop_condition)

def compile_animation_data(
Expand Down
43 changes: 35 additions & 8 deletions tests/module/animation/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
from manim import FadeIn, Scene


@pytest.mark.parametrize(
"run_time",
[0, -1],
)
def test_animation_forbidden_run_time(run_time):
def test_animation_zero_total_run_time():
test_scene = Scene()
with pytest.raises(
ValueError, match="Please set the run_time to a positive number."
ValueError, match="The total run_time must be a positive number."
):
test_scene.play(FadeIn(None, run_time=run_time))
test_scene.play(FadeIn(None, run_time=0))


def test_single_animation_zero_run_time_with_more_animations():
test_scene = Scene()
test_scene.play(FadeIn(None, run_time=0), FadeIn(None, run_time=1))


def test_animation_negative_run_time():
with pytest.raises(ValueError, match="The run_time of FadeIn cannot be negative."):
FadeIn(None, run_time=-1)


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


@pytest.mark.parametrize("duration", [0, -1])
def test_wait_invalid_duration(duration):
test_scene = Scene()
with pytest.raises(ValueError, match="The duration must be a positive number."):
test_scene.wait(duration)


@pytest.mark.parametrize("frozen_frame", [False, True])
def test_wait_run_time_shorter_than_frame_rate(manim_caplog, frozen_frame):
def test_wait_duration_shorter_than_frame_rate(manim_caplog, frozen_frame):
test_scene = Scene()
test_scene.wait(1e-9, frozen_frame=frozen_frame)
assert "too short for the current frame rate" in manim_caplog.text


@pytest.mark.parametrize("duration", [0, -1])
def test_pause_invalid_duration(duration):
test_scene = Scene()
with pytest.raises(ValueError, match="The duration must be a positive number."):
test_scene.pause(duration)


@pytest.mark.parametrize("max_time", [0, -1])
def test_wait_until_invalid_max_time(max_time):
test_scene = Scene()
with pytest.raises(ValueError, match="The max_time must be a positive number."):
test_scene.wait_until(lambda: True, max_time)

0 comments on commit f81d571

Please sign in to comment.