feat(lib): add loop option to next_slide and remove start/end_loop (#294)

* feat(lib): add `loop` option to `next_slide` and remove `start/end_loop`

* fix(docs): PR number
This commit is contained in:
Jérome Eertmans
2023-10-19 15:37:54 +02:00
committed by GitHub
parent 7928f6020c
commit 0322dae743
9 changed files with 78 additions and 117 deletions

View File

@ -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)
<!-- end changelog -->

View File

@ -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))
```

View File

@ -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,

View File

@ -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))"
]
},

View File

@ -16,11 +16,10 @@ class BasicExample(Slide):
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 @@ class Example(Slide):
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 @@ if not MANIMGL:
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 @@ if not MANIMGL:
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 @@ else:
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 @@ else:
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))

View File

@ -29,10 +29,10 @@ class BaseSlide:
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 @@ class BaseSlide:
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 @@ class BaseSlide:
.. 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 @@ class BaseSlide:
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 @@ class BaseSlide:
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]
def end_loop(self) -> None:
"""
End an existing loop.
self._slides.append(
PreSlideConfig(
start_animation=self._start_animation,
end_animation=self._current_animation,
**self._pre_slide_config_kwargs,
)
)
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:
"""

View File

@ -108,7 +108,7 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
bye = Text("Bye!")
self.start_loop()
self.next_slide(loop=True)
self.wipe(
self.mobjects_without_canvas,
[bye],
@ -121,7 +121,7 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
direction=DOWN
)
self.wait(.5)
self.end_loop()
self.next_slide()
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
"""

View File

@ -26,12 +26,12 @@ class BasicSlide(Slide):
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])

View File

@ -21,7 +21,6 @@ from manim import (
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 @@ class TestSlide:
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 @@ class TestSlide:
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):