Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 -->
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
```
Expand Down
2 changes: 0 additions & 2 deletions docs/source/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions docs/source/reference/magic_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
]
},
Expand Down
33 changes: 16 additions & 17 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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))

Expand Down Expand Up @@ -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))
Expand All @@ -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))

Expand Down
109 changes: 35 additions & 74 deletions manim_slides/slide/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
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

Expand All @@ -45,49 +45,49 @@
@abstractmethod
def _frame_height(self) -> float:
"""Return the scene's frame height."""
...

Check warning on line 48 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L48

Added line #L48 was not covered by tests

@property
@abstractmethod
def _frame_width(self) -> float:
"""Return the scene's frame width."""
...

Check warning on line 54 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L54

Added line #L54 was not covered by tests

@property
@abstractmethod
def _background_color(self) -> str:
"""Return the scene's background color."""
...

Check warning on line 60 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L60

Added line #L60 was not covered by tests

@property
@abstractmethod
def _resolution(self) -> Tuple[int, int]:
"""Return the scene's resolution used during rendering."""
...

Check warning on line 66 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L66

Added line #L66 was not covered by tests

@property
@abstractmethod
def _partial_movie_files(self) -> List[Path]:
"""Return a list of partial movie files, a.k.a animations."""
...

Check warning on line 72 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L72

Added line #L72 was not covered by tests

@property
@abstractmethod
def _show_progress_bar(self) -> bool:
"""Return True if progress bar should be displayed."""
...

Check warning on line 78 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L78

Added line #L78 was not covered by tests

@property
@abstractmethod
def _leave_progress_bar(self) -> bool:
"""Return True if progress bar should be left after completed."""
...

Check warning on line 84 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L84

Added line #L84 was not covered by tests

@property
@abstractmethod
def _start_at_animation_number(self) -> Optional[int]:
"""If set, return the animation number at which rendering start."""
...

Check warning on line 90 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L90

Added line #L90 was not covered by tests

@property
def canvas(self) -> MutableMapping[str, Mobject]:
Expand Down Expand Up @@ -245,20 +245,23 @@

@wait_time_between_slides.setter
def wait_time_between_slides(self, wait_time: float) -> None:
self._wait_time_between_slides = max(wait_time, 0.0)

Check warning on line 248 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L248

Added line #L248 was not covered by tests

def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload `self.play` and increment animation count."""
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::

Expand All @@ -267,7 +270,8 @@

.. 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
--------
Expand All @@ -290,58 +294,7 @@

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
Expand All @@ -354,38 +307,46 @@
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]

Check warning on line 321 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L321

Added line #L321 was not covered by tests

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

Check warning on line 341 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L341

Added line #L341 was not covered by tests

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:
"""
Expand All @@ -406,12 +367,12 @@

# We must filter slides that end before the animation offset
if offset := self._start_at_animation_number:
self._slides = [

Check warning on line 370 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L370

Added line #L370 was not covered by tests
slide for slide in self._slides if slide.end_animation > offset
]
for slide in self._slides:
slide.start_animation = max(0, slide.start_animation - offset)
slide.end_animation -= offset

Check warning on line 375 in manim_slides/slide/base.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/base.py#L373-L375

Added lines #L373 - L375 were not covered by tests

slides: List[SlideConfig] = []

Expand Down
4 changes: 2 additions & 2 deletions manim_slides/slide/manim.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
# ffmpeg was stored as a constant in manim.constants
try:
return Path(config.ffmpeg_executable)
except AttributeError:
return super()._ffmpeg_bin

Check warning on line 22 in manim_slides/slide/manim.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/manim.py#L21-L22

Added lines #L21 - L22 were not covered by tests

@property
def _frame_height(self) -> float:
Expand All @@ -35,7 +35,7 @@
if hex_color := getattr(color, "hex", None):
return hex_color # type: ignore
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
return color.to_hex() # type: ignore

Check warning on line 38 in manim_slides/slide/manim.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/slide/manim.py#L38

Added line #L38 was not covered by tests

@property
def _resolution(self) -> Tuple[int, int]:
Expand Down Expand Up @@ -108,7 +108,7 @@

bye = Text("Bye!")

self.start_loop()
self.next_slide(loop=True)
self.wipe(
self.mobjects_without_canvas,
[bye],
Expand All @@ -121,7 +121,7 @@
direction=DOWN
)
self.wait(.5)
self.end_loop()
self.next_slide()

self.play(*[FadeOut(mobject) for mobject in self.mobjects])
"""
Expand Down
6 changes: 3 additions & 3 deletions tests/data/slides.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Loading
Loading