diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e4796fe..5c090f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ In an effort to better document changes, this CHANGELOG document is now created. [#285](https://github.com/jeertmans/manim-slides/pull/285) - Added a working `ThreeDSlide` class compatible with `manimlib`. [#285](https://github.com/jeertmans/manim-slides/pull/285) +- Added `loop` option to `Slide`'s `next_slide` method. + Calling `next_slide` will never fail anymore. + [#294](https://github.com/jeertmans/manim-slides/pull/294) ### Changed @@ -102,5 +105,9 @@ In an effort to better document changes, this CHANGELOG document is now created. [#243](https://github.com/jeertmans/manim-slides/pull/243) - Removed `PERF` verbosity level because not used anymore. [#245](https://github.com/jeertmans/manim-slides/pull/245) +- Remove `Slide`'s method `start_loop` and `self.end_loop` + in favor to `self.next_slide(loop=True)`. + This is a **breaking change**. + [#294](https://github.com/jeertmans/manim-slides/pull/294) diff --git a/README.md b/README.md index e1134c3c..d788abb3 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,9 @@ The documentation is available [online](https://eertmans.be/manim-slides/). ### Basic Example -Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue): +Call `self.next_slide()` everytime you want to create a pause between +animations, and `self.next_slide(loop=True)` if you want the next slide to loop +over animations until the user presses continue: ```python # example.py @@ -107,9 +109,9 @@ class BasicExample(Slide): self.play(GrowFromCenter(circle)) self.next_slide() # Waits user to press continue to go to the next slide - self.start_loop() # Start loop + self.next_slide(loop=True) # Start loop self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) - self.end_loop() # This will loop until user inputs a key + self.next_slide() # This will start a new non-looping slide self.play(dot.animate.move_to(ORIGIN)) ``` diff --git a/docs/source/reference/api.md b/docs/source/reference/api.md index 43e6855a..19e2b7df 100644 --- a/docs/source/reference/api.md +++ b/docs/source/reference/api.md @@ -14,11 +14,9 @@ use, not the methods used internally when rendering. add_to_canvas, canvas, canvas_mobjects, - end_loop, mobjects_without_canvas, next_slide, remove_from_canvas, - start_loop, wait_time_between_slides, wipe, zoom, diff --git a/docs/source/reference/magic_example.ipynb b/docs/source/reference/magic_example.ipynb index 5afffa18..94a6036a 100644 --- a/docs/source/reference/magic_example.ipynb +++ b/docs/source/reference/magic_example.ipynb @@ -58,10 +58,9 @@ " ).arrange(DOWN, buff=1.)\n", " \n", " self.play(Write(text))\n", - " self.next_slide()\n", - " self.start_loop()\n", + " self.next_slide(loop=True)\n", " self.play(Indicate(text[-1], scale_factor=2., run_time=.5))\n", - " self.end_loop()\n", + " self.next_slide()\n", " self.play(FadeOut(text))" ] }, diff --git a/example.py b/example.py index 9a7fc392..daf9c304 100644 --- a/example.py +++ b/example.py @@ -16,11 +16,10 @@ def construct(self): dot = Dot() self.play(GrowFromCenter(circle)) - self.next_slide() # Waits user to press continue to go to the next slide - self.start_loop() # Start loop + self.next_slide(loop=True) self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) - self.end_loop() # This will loop until user inputs a key + self.next_slide() self.play(dot.animate.move_to(ORIGIN)) @@ -137,9 +136,9 @@ class Example(Slide): def construct(self): dot = Dot() self.add(dot) - self.start_loop() + self.next_slide(loop=True) self.play(Indicate(dot, scale_factor=2)) - self.end_loop() + self.next_slide() square = Square() self.play(Transform(dot, square)) self.next_slide() @@ -195,17 +194,17 @@ def construct(self): watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5) - self.start_loop() + self.next_slide(loop=True) self.play(FadeIn(watch_text)) self.play(FadeOut(watch_text)) - self.end_loop() + self.next_slide() self.clear() dot = Dot() self.add(dot) - self.start_loop() + self.next_slide(loop=True) self.play(Indicate(dot, scale_factor=2)) - self.end_loop() + self.next_slide() square = Square() self.play(Transform(dot, square)) self.remove(dot) @@ -245,9 +244,9 @@ def construct(self): self.next_slide() - self.start_loop() + self.next_slide(loop=True) self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear) - self.end_loop() + self.next_slide() self.stop_ambient_camera_rotation() self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) @@ -258,9 +257,9 @@ def construct(self): self.play(dot.animate.move_to(RIGHT * 3)) self.next_slide() - self.start_loop() + self.next_slide(loop=True) self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) - self.end_loop() + self.next_slide() self.play(dot.animate.move_to(ORIGIN)) @@ -292,9 +291,9 @@ def updater(m, dt): self.next_slide() - self.start_loop() + self.next_slide(loop=True) self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear) - self.end_loop() + self.next_slide() frame.remove_updater(updater) self.play(frame.animate.set_theta(30 * DEGREES)) @@ -304,9 +303,9 @@ def updater(m, dt): self.play(dot.animate.move_to(RIGHT * 3)) self.next_slide() - self.start_loop() + self.next_slide(loop=True) self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) - self.end_loop() + self.next_slide() self.play(dot.animate.move_to(ORIGIN)) diff --git a/manim_slides/slide/base.py b/manim_slides/slide/base.py index 3b174c26..f7e40a76 100644 --- a/manim_slides/slide/base.py +++ b/manim_slides/slide/base.py @@ -29,10 +29,10 @@ def __init__( super().__init__(*args, **kwargs) self._output_folder: Path = output_folder self._slides: List[PreSlideConfig] = [] + self._pre_slide_config_kwargs: MutableMapping[str, Any] = {} self._current_slide = 1 self._current_animation = 0 - self._loop_start_animation: Optional[int] = None - self._pause_start_animation = 0 + self._start_animation = 0 self._canvas: MutableMapping[str, Mobject] = {} self._wait_time_between_slides = 0.0 @@ -252,13 +252,16 @@ def play(self, *args: Any, **kwargs: Any) -> None: super().play(*args, **kwargs) # type: ignore[misc] self._current_animation += 1 - def next_slide(self) -> None: + def next_slide(self, loop: bool = False) -> None: """ - Create a new slide with previous animations. + Create a new slide with previous animations, and setup options + for the next slide. This usually means that the user will need to press some key before the next slide is played. By default, this is the right arrow key. + :param loop: + If set, next slide will be looping. .. note:: @@ -267,7 +270,8 @@ def next_slide(self) -> None: .. warning:: - This is not allowed to call :func:`next_slide` inside a loop. + When rendered with RevealJS, loops cannot be in the first nor + the last slide. Examples -------- @@ -290,58 +294,7 @@ def construct(self): self.next_slide() self.play(FadeOut(text)) - """ - assert ( - self._loop_start_animation is None - ), "You cannot call `self.next_slide()` inside a loop" - - if self.wait_time_between_slides > 0.0: - self.wait(self.wait_time_between_slides) # type: ignore[attr-defined] - - self._slides.append( - PreSlideConfig( - start_animation=self._pause_start_animation, - end_animation=self._current_animation, - ) - ) - self._current_slide += 1 - self._pause_start_animation = self._current_animation - - def _add_last_slide(self) -> None: - """Add a 'last' slide to the end of slides.""" - if ( - len(self._slides) > 0 - and self._current_animation == self._slides[-1].end_animation - ): - return - - self._slides.append( - PreSlideConfig( - start_animation=self._pause_start_animation, - end_animation=self._current_animation, - loop=self._loop_start_animation is not None, - ) - ) - - def start_loop(self) -> None: - """ - Start a loop. End it with :func:`end_loop`. - A loop will automatically replay the slide, i.e., everything between - :func:`start_loop` and :func:`end_loop`, upon reaching end. - - .. warning:: - - You should always call :func:`next_slide` before calling this - method. Otherwise, ... - - .. warning:: - - When rendered with RevealJS, loops cannot be in the first nor - the last slide. - - Examples - -------- The following contains one slide that will loop endlessly. .. manim-slides:: LoopExample @@ -354,38 +307,46 @@ def construct(self): dot = Dot(color=BLUE, radius=1) self.play(FadeIn(dot)) - self.next_slide() - self.start_loop() + self.next_slide(loop=True) self.play(Indicate(dot, scale_factor=2)) - self.end_loop() + self.next_slide() self.play(FadeOut(dot)) """ - assert self._loop_start_animation is None, "You cannot nest loops" - self._loop_start_animation = self._current_animation + if self._current_animation > self._start_animation: + if self.wait_time_between_slides > 0.0: + self.wait(self.wait_time_between_slides) # type: ignore[attr-defined] + + self._slides.append( + PreSlideConfig( + start_animation=self._start_animation, + end_animation=self._current_animation, + **self._pre_slide_config_kwargs, + ) + ) - def end_loop(self) -> None: - """ - End an existing loop. + self._pre_slide_config_kwargs = dict(loop=loop) + self._current_slide += 1 + self._start_animation = self._current_animation + + def _add_last_slide(self) -> None: + """Add a 'last' slide to the end of slides.""" + if ( + len(self._slides) > 0 + and self._current_animation == self._slides[-1].end_animation + ): + return - See :func:`start_loop` for more details. - """ - assert ( - self._loop_start_animation is not None - ), "You have to start a loop before ending it" self._slides.append( PreSlideConfig( - start_animation=self._loop_start_animation, + start_animation=self._start_animation, end_animation=self._current_animation, - loop=True, + **self._pre_slide_config_kwargs, ) ) - self._current_slide += 1 - self._loop_start_animation = None - self._pause_start_animation = self._current_animation def _save_slides(self, use_cache: bool = True) -> None: """ diff --git a/manim_slides/slide/manim.py b/manim_slides/slide/manim.py index ab797786..11d262a0 100644 --- a/manim_slides/slide/manim.py +++ b/manim_slides/slide/manim.py @@ -108,7 +108,7 @@ def construct(self): bye = Text("Bye!") - self.start_loop() + self.next_slide(loop=True) self.wipe( self.mobjects_without_canvas, [bye], @@ -121,7 +121,7 @@ def construct(self): direction=DOWN ) self.wait(.5) - self.end_loop() + self.next_slide() self.play(*[FadeOut(mobject) for mobject in self.mobjects]) """ diff --git a/tests/data/slides.py b/tests/data/slides.py index 197a6620..0aeebe27 100644 --- a/tests/data/slides.py +++ b/tests/data/slides.py @@ -26,12 +26,12 @@ def construct(self): self.play(FadeIn(square)) - self.next_slide() + self.next_slide(loop=True) - self.start_loop() self.play(Rotate(square, +PI / 2)) self.play(Rotate(square, -PI / 2)) - self.end_loop() + + self.next_slide() other_text = Text("Other text") self.wipe([square, circle], [other_text]) diff --git a/tests/test_slide.py b/tests/test_slide.py index 7388d454..98129184 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -21,7 +21,6 @@ Text, ) from manim.__main__ import main as manim_cli -from pydantic import ValidationError from manim_slides.config import PresentationConfig from manim_slides.defaults import FOLDER_PATH @@ -89,7 +88,7 @@ def test_render_basic_slide( def assert_constructs(cls: type) -> type: class Wrapper: @classmethod - def test_render(_) -> None: # noqa: N804 + def test_construct(_) -> None: # noqa: N804 cls().construct() return Wrapper @@ -111,8 +110,7 @@ def construct(self) -> None: assert self._output_folder == FOLDER_PATH assert len(self._slides) == 0 assert self._current_slide == 1 - assert self._loop_start_animation is None - assert self._pause_start_animation == 0 + assert self._start_animation == 0 assert len(self._canvas) == 0 assert self._wait_time_between_slides == 0.0 @@ -156,19 +154,16 @@ def construct(self) -> None: self.add(text) - self.start_loop() + assert "loop" not in self._pre_slide_config_kwargs + + self.next_slide(loop=True) self.play(text.animate.scale(2)) - self.end_loop() - with pytest.raises(AssertionError): - self.end_loop() + assert self._pre_slide_config_kwargs["loop"] - self.start_loop() - with pytest.raises(AssertionError): - self.start_loop() + self.next_slide(loop=False) - with pytest.raises(ValidationError): - self.end_loop() + assert not self._pre_slide_config_kwargs["loop"] @assert_constructs class TestWipe(Slide):