mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-17 18:55:53 +08:00
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:
@ -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 -->
|
||||
|
@ -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))
|
||||
```
|
||||
|
@ -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,
|
||||
|
@ -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))"
|
||||
]
|
||||
},
|
||||
|
33
example.py
33
example.py
@ -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))
|
||||
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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])
|
||||
"""
|
||||
|
@ -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])
|
||||
|
@ -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):
|
||||
|
Reference in New Issue
Block a user