mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-07-04 07:27:00 +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)
|
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||||
- Added a working `ThreeDSlide` class compatible with `manimlib`.
|
- Added a working `ThreeDSlide` class compatible with `manimlib`.
|
||||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
[#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
|
### 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)
|
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||||
- Removed `PERF` verbosity level because not used anymore.
|
- Removed `PERF` verbosity level because not used anymore.
|
||||||
[#245](https://github.com/jeertmans/manim-slides/pull/245)
|
[#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 -->
|
<!-- end changelog -->
|
||||||
|
@ -89,7 +89,9 @@ The documentation is available [online](https://eertmans.be/manim-slides/).
|
|||||||
|
|
||||||
### Basic Example
|
### 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
|
```python
|
||||||
# example.py
|
# example.py
|
||||||
@ -107,9 +109,9 @@ class BasicExample(Slide):
|
|||||||
self.play(GrowFromCenter(circle))
|
self.play(GrowFromCenter(circle))
|
||||||
self.next_slide() # Waits user to press continue to go to the next slide
|
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.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))
|
self.play(dot.animate.move_to(ORIGIN))
|
||||||
```
|
```
|
||||||
|
@ -14,11 +14,9 @@ use, not the methods used internally when rendering.
|
|||||||
add_to_canvas,
|
add_to_canvas,
|
||||||
canvas,
|
canvas,
|
||||||
canvas_mobjects,
|
canvas_mobjects,
|
||||||
end_loop,
|
|
||||||
mobjects_without_canvas,
|
mobjects_without_canvas,
|
||||||
next_slide,
|
next_slide,
|
||||||
remove_from_canvas,
|
remove_from_canvas,
|
||||||
start_loop,
|
|
||||||
wait_time_between_slides,
|
wait_time_between_slides,
|
||||||
wipe,
|
wipe,
|
||||||
zoom,
|
zoom,
|
||||||
|
@ -58,10 +58,9 @@
|
|||||||
" ).arrange(DOWN, buff=1.)\n",
|
" ).arrange(DOWN, buff=1.)\n",
|
||||||
" \n",
|
" \n",
|
||||||
" self.play(Write(text))\n",
|
" self.play(Write(text))\n",
|
||||||
" self.next_slide()\n",
|
" self.next_slide(loop=True)\n",
|
||||||
" self.start_loop()\n",
|
|
||||||
" self.play(Indicate(text[-1], scale_factor=2., run_time=.5))\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))"
|
" self.play(FadeOut(text))"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
33
example.py
33
example.py
@ -16,11 +16,10 @@ class BasicExample(Slide):
|
|||||||
dot = Dot()
|
dot = Dot()
|
||||||
|
|
||||||
self.play(GrowFromCenter(circle))
|
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.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))
|
self.play(dot.animate.move_to(ORIGIN))
|
||||||
|
|
||||||
@ -137,9 +136,9 @@ class Example(Slide):
|
|||||||
def construct(self):
|
def construct(self):
|
||||||
dot = Dot()
|
dot = Dot()
|
||||||
self.add(dot)
|
self.add(dot)
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.play(Indicate(dot, scale_factor=2))
|
self.play(Indicate(dot, scale_factor=2))
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
square = Square()
|
square = Square()
|
||||||
self.play(Transform(dot, square))
|
self.play(Transform(dot, square))
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
@ -195,17 +194,17 @@ class Example(Slide):
|
|||||||
|
|
||||||
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
|
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(FadeIn(watch_text))
|
||||||
self.play(FadeOut(watch_text))
|
self.play(FadeOut(watch_text))
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
dot = Dot()
|
dot = Dot()
|
||||||
self.add(dot)
|
self.add(dot)
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.play(Indicate(dot, scale_factor=2))
|
self.play(Indicate(dot, scale_factor=2))
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
square = Square()
|
square = Square()
|
||||||
self.play(Transform(dot, square))
|
self.play(Transform(dot, square))
|
||||||
self.remove(dot)
|
self.remove(dot)
|
||||||
@ -245,9 +244,9 @@ if not MANIMGL:
|
|||||||
|
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
|
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
|
|
||||||
self.stop_ambient_camera_rotation()
|
self.stop_ambient_camera_rotation()
|
||||||
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
|
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.play(dot.animate.move_to(RIGHT * 3))
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
|
|
||||||
self.play(dot.animate.move_to(ORIGIN))
|
self.play(dot.animate.move_to(ORIGIN))
|
||||||
|
|
||||||
@ -292,9 +291,9 @@ else:
|
|||||||
|
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
|
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
|
|
||||||
frame.remove_updater(updater)
|
frame.remove_updater(updater)
|
||||||
self.play(frame.animate.set_theta(30 * DEGREES))
|
self.play(frame.animate.set_theta(30 * DEGREES))
|
||||||
@ -304,9 +303,9 @@ else:
|
|||||||
self.play(dot.animate.move_to(RIGHT * 3))
|
self.play(dot.animate.move_to(RIGHT * 3))
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
|
|
||||||
self.play(dot.animate.move_to(ORIGIN))
|
self.play(dot.animate.move_to(ORIGIN))
|
||||||
|
|
||||||
|
@ -29,10 +29,10 @@ class BaseSlide:
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._output_folder: Path = output_folder
|
self._output_folder: Path = output_folder
|
||||||
self._slides: List[PreSlideConfig] = []
|
self._slides: List[PreSlideConfig] = []
|
||||||
|
self._pre_slide_config_kwargs: MutableMapping[str, Any] = {}
|
||||||
self._current_slide = 1
|
self._current_slide = 1
|
||||||
self._current_animation = 0
|
self._current_animation = 0
|
||||||
self._loop_start_animation: Optional[int] = None
|
self._start_animation = 0
|
||||||
self._pause_start_animation = 0
|
|
||||||
self._canvas: MutableMapping[str, Mobject] = {}
|
self._canvas: MutableMapping[str, Mobject] = {}
|
||||||
self._wait_time_between_slides = 0.0
|
self._wait_time_between_slides = 0.0
|
||||||
|
|
||||||
@ -252,13 +252,16 @@ class BaseSlide:
|
|||||||
super().play(*args, **kwargs) # type: ignore[misc]
|
super().play(*args, **kwargs) # type: ignore[misc]
|
||||||
self._current_animation += 1
|
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
|
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.
|
next slide is played. By default, this is the right arrow key.
|
||||||
|
|
||||||
|
:param loop:
|
||||||
|
If set, next slide will be looping.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@ -267,7 +270,8 @@ class BaseSlide:
|
|||||||
|
|
||||||
.. warning::
|
.. 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
|
Examples
|
||||||
--------
|
--------
|
||||||
@ -290,58 +294,7 @@ class BaseSlide:
|
|||||||
|
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
self.play(FadeOut(text))
|
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.
|
The following contains one slide that will loop endlessly.
|
||||||
|
|
||||||
.. manim-slides:: LoopExample
|
.. manim-slides:: LoopExample
|
||||||
@ -354,38 +307,46 @@ class BaseSlide:
|
|||||||
dot = Dot(color=BLUE, radius=1)
|
dot = Dot(color=BLUE, radius=1)
|
||||||
|
|
||||||
self.play(FadeIn(dot))
|
self.play(FadeIn(dot))
|
||||||
self.next_slide()
|
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
|
|
||||||
self.play(Indicate(dot, scale_factor=2))
|
self.play(Indicate(dot, scale_factor=2))
|
||||||
|
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
|
|
||||||
self.play(FadeOut(dot))
|
self.play(FadeOut(dot))
|
||||||
"""
|
"""
|
||||||
assert self._loop_start_animation is None, "You cannot nest loops"
|
if self._current_animation > self._start_animation:
|
||||||
self._loop_start_animation = self._current_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:
|
self._slides.append(
|
||||||
"""
|
PreSlideConfig(
|
||||||
End an existing loop.
|
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(
|
self._slides.append(
|
||||||
PreSlideConfig(
|
PreSlideConfig(
|
||||||
start_animation=self._loop_start_animation,
|
start_animation=self._start_animation,
|
||||||
end_animation=self._current_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:
|
def _save_slides(self, use_cache: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -108,7 +108,7 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
|
|||||||
|
|
||||||
bye = Text("Bye!")
|
bye = Text("Bye!")
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=True)
|
||||||
self.wipe(
|
self.wipe(
|
||||||
self.mobjects_without_canvas,
|
self.mobjects_without_canvas,
|
||||||
[bye],
|
[bye],
|
||||||
@ -121,7 +121,7 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
|
|||||||
direction=DOWN
|
direction=DOWN
|
||||||
)
|
)
|
||||||
self.wait(.5)
|
self.wait(.5)
|
||||||
self.end_loop()
|
self.next_slide()
|
||||||
|
|
||||||
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
|
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
|
||||||
"""
|
"""
|
||||||
|
@ -26,12 +26,12 @@ class BasicSlide(Slide):
|
|||||||
|
|
||||||
self.play(FadeIn(square))
|
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.play(Rotate(square, -PI / 2))
|
self.play(Rotate(square, -PI / 2))
|
||||||
self.end_loop()
|
|
||||||
|
self.next_slide()
|
||||||
|
|
||||||
other_text = Text("Other text")
|
other_text = Text("Other text")
|
||||||
self.wipe([square, circle], [other_text])
|
self.wipe([square, circle], [other_text])
|
||||||
|
@ -21,7 +21,6 @@ from manim import (
|
|||||||
Text,
|
Text,
|
||||||
)
|
)
|
||||||
from manim.__main__ import main as manim_cli
|
from manim.__main__ import main as manim_cli
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
from manim_slides.config import PresentationConfig
|
from manim_slides.config import PresentationConfig
|
||||||
from manim_slides.defaults import FOLDER_PATH
|
from manim_slides.defaults import FOLDER_PATH
|
||||||
@ -89,7 +88,7 @@ def test_render_basic_slide(
|
|||||||
def assert_constructs(cls: type) -> type:
|
def assert_constructs(cls: type) -> type:
|
||||||
class Wrapper:
|
class Wrapper:
|
||||||
@classmethod
|
@classmethod
|
||||||
def test_render(_) -> None: # noqa: N804
|
def test_construct(_) -> None: # noqa: N804
|
||||||
cls().construct()
|
cls().construct()
|
||||||
|
|
||||||
return Wrapper
|
return Wrapper
|
||||||
@ -111,8 +110,7 @@ class TestSlide:
|
|||||||
assert self._output_folder == FOLDER_PATH
|
assert self._output_folder == FOLDER_PATH
|
||||||
assert len(self._slides) == 0
|
assert len(self._slides) == 0
|
||||||
assert self._current_slide == 1
|
assert self._current_slide == 1
|
||||||
assert self._loop_start_animation is None
|
assert self._start_animation == 0
|
||||||
assert self._pause_start_animation == 0
|
|
||||||
assert len(self._canvas) == 0
|
assert len(self._canvas) == 0
|
||||||
assert self._wait_time_between_slides == 0.0
|
assert self._wait_time_between_slides == 0.0
|
||||||
|
|
||||||
@ -156,19 +154,16 @@ class TestSlide:
|
|||||||
|
|
||||||
self.add(text)
|
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.play(text.animate.scale(2))
|
||||||
self.end_loop()
|
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
assert self._pre_slide_config_kwargs["loop"]
|
||||||
self.end_loop()
|
|
||||||
|
|
||||||
self.start_loop()
|
self.next_slide(loop=False)
|
||||||
with pytest.raises(AssertionError):
|
|
||||||
self.start_loop()
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
assert not self._pre_slide_config_kwargs["loop"]
|
||||||
self.end_loop()
|
|
||||||
|
|
||||||
@assert_constructs
|
@assert_constructs
|
||||||
class TestWipe(Slide):
|
class TestWipe(Slide):
|
||||||
|
Reference in New Issue
Block a user