feat(lib): add skip_animations compatibility (#516)

* feat: Add skip_animations compatibility

* Add tests, config and changelog

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update manim_slides/slide/base.py

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(tests): implement tests

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rodrigo Martín
2025-01-21 11:27:21 +01:00
committed by GitHub
parent df31345f83
commit 32ab690932
6 changed files with 107 additions and 5 deletions

View File

@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(unreleased)= (unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.1...HEAD) ## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.1...HEAD)
(unreleased-added)=
### Added
- Added `skip_animations` compatibility with ManimCE.
[@Rapsssito](https://github.com/Rapsssito) [#516](https://github.com/jeertmans/manim-slides/pull/516)
(v5.3.1)= (v5.3.1)=
## [v5.3.1](https://github.com/jeertmans/manim-slides/compare/v5.3.0...v5.3.1) ## [v5.3.1](https://github.com/jeertmans/manim-slides/compare/v5.3.0...v5.3.1)

View File

@ -160,6 +160,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
reversed_playback_rate: float = 1.0 reversed_playback_rate: float = 1.0
notes: str = "" notes: str = ""
dedent_notes: bool = True dedent_notes: bool = True
skip_animations: bool = False
@classmethod @classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]: def wrapper(cls, arg_name: str) -> Callable[..., Any]:

View File

@ -277,7 +277,7 @@ class BaseSlide:
self._wait_time_between_slides = max(wait_time, 0.0) self._wait_time_between_slides = max(wait_time, 0.0)
def play(self, *args: Any, **kwargs: Any) -> None: def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload `self.play` and increment animation count.""" """Overload 'self.play' and increment animation count."""
super().play(*args, **kwargs) # type: ignore[misc] super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1 self._current_animation += 1
@ -299,6 +299,11 @@ class BaseSlide:
Positional arguments passed to Positional arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`, :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used. or ignored if `manimlib` API is used.
:param skip_animations:
Exclude the next slide from the output.
If `manim` is used, this is also passed to `:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
which will avoid rendering the corresponding animations.
:param loop: :param loop:
If set, next slide will be looping. If set, next slide will be looping.
:param auto_next: :param auto_next:
@ -521,9 +526,16 @@ class BaseSlide:
ascii=True if platform.system() == "Windows" else None, ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar, disable=not self._show_progress_bar,
): ):
if pre_slide_config.skip_animations:
continue
slide_files = files[pre_slide_config.slides_slice] slide_files = files[pre_slide_config.slides_slice]
file = merge_basenames(slide_files) try:
file = merge_basenames(slide_files)
except ValueError as e:
raise ValueError(
f"Failed to merge basenames of files for slide: {pre_slide_config!r}"
) from e
dst_file = scene_files_folder / file.name dst_file = scene_files_folder / file.name
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}" rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"

View File

@ -89,6 +89,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
def _start_at_animation_number(self) -> Optional[int]: def _start_at_animation_number(self) -> Optional[int]:
return config["from_animation_number"] # type: ignore return config["from_animation_number"] # type: ignore
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload 'self.play' and increment animation count."""
super().play(*args, **kwargs)
if self._base_slide_config.skip_animations:
# Manim will not render the animations, so we reset the animation
# counter to the previous value
self._current_animation -= 1
def next_section(self, *args: Any, **kwargs: Any) -> None: def next_section(self, *args: Any, **kwargs: Any) -> None:
""" """
Alias to :meth:`next_slide`. Alias to :meth:`next_slide`.
@ -111,7 +120,9 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
base_slide_config: BaseSlideConfig, base_slide_config: BaseSlideConfig,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
Scene.next_section(self, *args, **kwargs) Scene.next_section(
self, *args, skip_animations=base_slide_config.skip_animations, **kwargs
)
BaseSlide.next_slide.__wrapped__( BaseSlide.next_slide.__wrapped__(
self, self,
base_slide_config=base_slide_config, base_slide_config=base_slide_config,

View File

@ -1,6 +1,11 @@
import contextlib
import os
import random import random
import shutil import shutil
import sys import sys
import tempfile
import threading
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import Any, Union
@ -17,6 +22,7 @@ from manim import (
Dot, Dot,
FadeIn, FadeIn,
GrowFromCenter, GrowFromCenter,
Square,
Text, Text,
) )
from manim.renderer.opengl_renderer import OpenGLRenderer from manim.renderer.opengl_renderer import OpenGLRenderer
@ -229,8 +235,26 @@ def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct() init_slide(cls).construct()
_L = (
threading.Lock()
) # We cannot change directory multiple times at once (in the same thread)
@contextlib.contextmanager
def tmp_cwd() -> Iterator[str]:
old_cwd = os.getcwd()
with tempfile.TemporaryDirectory() as tmp_dir, _L:
try:
os.chdir(tmp_dir)
yield tmp_dir
finally:
os.chdir(old_cwd)
def assert_renders(cls: SlideType) -> None: def assert_renders(cls: SlideType) -> None:
init_slide(cls).render() with tmp_cwd():
init_slide(cls).render()
class TestSlide: class TestSlide:
@ -479,6 +503,53 @@ class TestSlide:
self.next_slide() self.next_slide()
assert self._current_slide == 2 assert self._current_slide == 2
def test_next_slide_skip_animations(self) -> None:
class Foo(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.next_slide(skip_animations=True)
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert self._base_slide_config.skip_animations
self.next_slide()
assert not self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
class Bar(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.next_slide(skip_animations=False)
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert not self._base_slide_config.skip_animations
self.next_slide()
assert not self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
with tmp_cwd() as tmp_dir:
init_slide(Foo).render()
init_slide(Bar).render()
slides_folder = Path(tmp_dir) / "slides"
assert slides_folder.exists()
slide_file = slides_folder / "Foo.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 2
slide_file = slides_folder / "Bar.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 3
def test_canvas(self) -> None: def test_canvas(self) -> None:
@assert_constructs @assert_constructs
class _(CESlide): class _(CESlide):

3
uv.lock generated
View File

@ -1482,7 +1482,6 @@ wheels = [
[[package]] [[package]]
name = "manim-slides" name = "manim-slides"
version = "5.3.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "av" }, { name = "av" },
@ -2762,6 +2761,8 @@ name = "pyqt6-qt6"
version = "6.8.1" version = "6.8.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/19/b89eb6cecbdf1e65a44658a083693a967e9d428370026711b624e928a8ca/PyQt6_Qt6-6.8.1-1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f4b8b55b1414b93f340f22e8c88d25550efcdebc4b65a3927dd947b73bd4358", size = 80877444 },
{ url = "https://files.pythonhosted.org/packages/87/1b/94d3710ee7ef93ee99c1dac512f631de5e310f6b21e43f474ef269f840b6/PyQt6_Qt6-6.8.1-1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:98aa99fe38ae68c5318284cd28f3479ba538c40bf6ece293980abae0925c1b24", size = 79473622 },
{ url = "https://files.pythonhosted.org/packages/df/0a/c47a1cc272b418faff8af79b121f0cecd32b09d634253254e3a990432220/PyQt6_Qt6-6.8.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:1eb8460a1fdb38d0b2458c2974c01d471c1e59e4eb19ea63fc447aaba3ad530e", size = 65754939 }, { url = "https://files.pythonhosted.org/packages/df/0a/c47a1cc272b418faff8af79b121f0cecd32b09d634253254e3a990432220/PyQt6_Qt6-6.8.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:1eb8460a1fdb38d0b2458c2974c01d471c1e59e4eb19ea63fc447aaba3ad530e", size = 65754939 },
{ url = "https://files.pythonhosted.org/packages/b1/e6/cc4fbc97a7d0955185e33add3ce00480f0023424d17ac6f864a504f60251/PyQt6_Qt6-6.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f3790c4ce4dc576e48b8718d55fb8743057e6cbd53a6ca1dd253ffbac9b7287", size = 59956028 }, { url = "https://files.pythonhosted.org/packages/b1/e6/cc4fbc97a7d0955185e33add3ce00480f0023424d17ac6f864a504f60251/PyQt6_Qt6-6.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f3790c4ce4dc576e48b8718d55fb8743057e6cbd53a6ca1dd253ffbac9b7287", size = 59956028 },
{ url = "https://files.pythonhosted.org/packages/01/22/c2997fe76d765d9ba960e9a099238cb419a316362bdde50fedacc23e7c7d/PyQt6_Qt6-6.8.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d6ca5d2b9d2ec0ee4a814b2175f641a5c4299cb80b45e0f5f8356632663f89b3", size = 72561636 }, { url = "https://files.pythonhosted.org/packages/01/22/c2997fe76d765d9ba960e9a099238cb419a316362bdde50fedacc23e7c7d/PyQt6_Qt6-6.8.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d6ca5d2b9d2ec0ee4a814b2175f641a5c4299cb80b45e0f5f8356632663f89b3", size = 72561636 },