Compare commits

..

10 Commits

Author SHA1 Message Date
528952dbc3 chore(deps): bump version from 5.4.1 to 5.4.2 2025-01-22 21:43:00 +01:00
dbced6e62e fix(lib): pass skip argument to ManimCE (#524)
* fix(lib): pass skip argument to ManimCE

* fix(lint): make ruff happy
2025-01-22 21:41:22 +01:00
941b895083 chore(deps): bump version from 5.4.0 to 5.4.1 2025-01-22 21:12:20 +01:00
289b7c1683 fix(ci): lockfile 2025-01-22 21:12:14 +01:00
b07a83898b feat(lib): add start_skip_animations and stop_skip_animations meth. (#523) 2025-01-22 21:10:37 +01:00
074a029759 chore(deps): bump version from 5.3.1 to 5.4.0 2025-01-21 13:41:51 +01:00
b4af76050e fix(ci): lock 2025-01-21 13:41:43 +01:00
adce58e1b7 fix(lib): bump Manim and fix OpenGL renderer (#522)
* fix(lib): bump Manim and fix OpenGL renderer

* fix(tests): correctly change cwd
2025-01-21 13:38:41 +01:00
32ab690932 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>
2025-01-21 11:27:21 +01:00
df31345f83 chore(deps): pre-commit autoupdate (#521)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.1 → v0.9.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.1...v0.9.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-21 10:11:13 +01:00
13 changed files with 826 additions and 416 deletions

View File

@ -21,7 +21,7 @@ repos:
exclude: poetry.lock exclude: poetry.lock
args: [--autofix, --trailing-commas] args: [--autofix, --trailing-commas]
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.1 rev: v0.9.2
hooks: hooks:
- id: ruff - id: ruff
args: [--fix] args: [--fix]

View File

@ -8,7 +8,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- start changelog --> <!-- start changelog -->
(unreleased)= (unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.1...HEAD) ## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.4.2...HEAD)
(v5.4.2)=
## [v5.4.2](https://github.com/jeertmans/manim-slides/compare/v5.4.1...v5.4.2)
(v5.4.2-fixed)=
### Fixed
- Fixed `start_skip_animations` to actually pass argument to ManimCE,
otherwise video animations were still rendered, just excluded from
the final output.
[#524](https://github.com/jeertmans/manim-slides/pull/524)
(v5.4.1)=
## [v5.4.1](https://github.com/jeertmans/manim-slides/compare/v5.4.0...v5.4.1)
(v5.4.1-added)=
### Added
- Added `start_skip_animations` and `stop_skip_animations` methods.
[#523](https://github.com/jeertmans/manim-slides/pull/523)
(v5.4.0)=
## [v5.4.0](https://github.com/jeertmans/manim-slides/compare/v5.3.1...v5.4.0)
(v5.4.0-added)=
### Added
- Added `skip_animations` compatibility with ManimCE.
[@Rapsssito](https://github.com/Rapsssito) [#516](https://github.com/jeertmans/manim-slides/pull/516)
(v5.4.0-chore)=
### Chore
- Bumped Manim to `>=0.19`, as it fixed OpenGL renderer issue.
[#522](https://github.com/jeertmans/manim-slides/pull/522)
(v5.4.0-fixed)=
### Fixed
- Fixed OpenGL renderer having no partial movie files with Manim bindings.
[#522](https://github.com/jeertmans/manim-slides/pull/522)
- Fixed `ConvertExample` example as `manim>=0.19` changed the `Code` class.
[#522](https://github.com/jeertmans/manim-slides/pull/522)
(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)
@ -54,11 +97,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.2.0-chore)= (v5.2.0-chore)=
### Chore ### Chore
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies - Bumped ManimGL to `>=1.7.1`, to remove conflicting dependencies
with Manim's. with Manim's.
[#499](https://github.com/jeertmans/manim-slides/pull/499) [#499](https://github.com/jeertmans/manim-slides/pull/499)
- Bump ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies, - Bumped ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies,
and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`, and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`,
as the latter includes many breaking changes. as the latter includes many breaking changes.
[#506](https://github.com/jeertmans/manim-slides/pull/506) [#506](https://github.com/jeertmans/manim-slides/pull/506)
@ -121,7 +164,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.1.8-chore)= (v5.1.8-chore)=
### Chore ### Chore
- Pin `rtoml==0.9.0` on Windows platforms, - Pinned `rtoml==0.9.0` on Windows platforms,
see [#398](https://github.com/jeertmans/manim-slides/pull/398), see [#398](https://github.com/jeertmans/manim-slides/pull/398),
until until
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74) [samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)

View File

@ -26,7 +26,7 @@ keywords:
- PowerPoint - PowerPoint
- Python - Python
license: MIT license: MIT
version: v5.3.1 version: v5.4.2
preferred-citation: preferred-citation:
publisher: publisher:
name: The Open Journal name: The Open Journal

View File

@ -18,6 +18,8 @@ use, not the methods used internally when rendering.
next_section, next_section,
next_slide, next_slide,
remove_from_canvas, remove_from_canvas,
start_skip_animations,
stop_skip_animations,
wait_time_between_slides, wait_time_between_slides,
wipe, wipe,
zoom, zoom,

View File

@ -53,7 +53,7 @@ class ConvertExample(Slide):
self.next_slide() self.next_slide()
code = Code( code = Code(
code="""from manim import * code_string="""from manim import *
class Example(Scene): class Example(Scene):
@ -72,7 +72,7 @@ class Example(Scene):
) )
code_step_1 = Code( code_step_1 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Scene): class Example(Scene):
@ -91,7 +91,7 @@ class Example(Scene):
) )
code_step_2 = Code( code_step_2 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Slide): class Example(Slide):
@ -110,7 +110,7 @@ class Example(Slide):
) )
code_step_3 = Code( code_step_3 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Slide): class Example(Slide):
@ -129,7 +129,7 @@ class Example(Slide):
) )
code_step_4 = Code( code_step_4 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Slide): class Example(Slide):
@ -148,19 +148,19 @@ class Example(Slide):
) )
code_step_5 = Code( code_step_5 = Code(
code="manim-slide render example.py Example", code_string="manim-slide render example.py Example",
language="console", language="console",
) )
code_step_6 = Code( code_step_6 = Code(
code="manim-slides Example", code_string="manim-slides Example",
language="console", language="console",
) )
or_text = Text("or generate HTML presentation").scale(0.5) or_text = Text("or generate HTML presentation").scale(0.5)
code_step_7 = Code( code_step_7 = Code(
code="manim-slides convert Example slides.html --open", code_string="manim-slides convert Example slides.html --open",
language="console", language="console",
).shift(DOWN) ).shift(DOWN)

View File

@ -1 +1 @@
__version__ = "5.3.1" __version__ = "5.4.2"

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

@ -49,6 +49,7 @@ class BaseSlide:
self._start_animation = 0 self._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
self._skip_animations = False
@property @property
@abstractmethod @abstractmethod
@ -277,7 +278,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 +300,16 @@ 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.
.. seealso::
:meth:`start_skip_animations`
:meth:`stop_skip_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:
@ -458,6 +469,9 @@ class BaseSlide:
self._current_slide += 1 self._current_slide += 1
if self._skip_animations:
base_slide_config.skip_animations = True
self._base_slide_config = base_slide_config self._base_slide_config = base_slide_config
self._start_animation = self._current_animation self._start_animation = self._current_animation
@ -521,9 +535,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}"
@ -560,6 +581,22 @@ class BaseSlide:
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'" f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
) )
def start_skip_animations(self) -> None:
"""
Start skipping animations.
This automatically applies ``skip_animations=True``
to all subsequent calls to :meth:`next_slide`.
This is useful when you want to skip animations from multiple slides in a row,
without having to manually set ``skip_animations=True``.
"""
self._skip_animations = True
def stop_skip_animations(self) -> None:
"""Stop skipping animations."""
self._skip_animations = False
def wipe( def wipe(
self, self,
*args: Any, *args: Any,

View File

@ -31,6 +31,12 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
for the current slide config. for the current slide config.
""" """
def __init__(self, *args: Any, **kwargs: Any) -> None:
# OpenGL renderer disables 'write_to_movie' by default
# which is required for saving the animations
config["write_to_movie"] = True
super().__init__(*args, **kwargs)
@property @property
def _frame_shape(self) -> tuple[float, float]: def _frame_shape(self) -> tuple[float, float]:
if isinstance(self.renderer, OpenGLRenderer): if isinstance(self.renderer, OpenGLRenderer):
@ -89,6 +95,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 +126,12 @@ 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 | self._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

@ -61,7 +61,7 @@ full = [
"manim-slides[magic,manim,sphinx-directive]", "manim-slides[magic,manim,sphinx-directive]",
] ]
magic = ["manim-slides[manim]", "ipython>=8.12.2"] magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17"] manim = ["manim>=0.19"]
manimgl = ["manimgl>=1.7.2"] manimgl = ["manimgl>=1.7.2"]
pyqt6 = ["pyqt6>=6.7.0"] pyqt6 = ["pyqt6>=6.7.0"]
pyqt6-full = ["manim-slides[full,pyqt6]"] pyqt6-full = ["manim-slides[full,pyqt6]"]
@ -91,7 +91,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
allow_dirty = false allow_dirty = false
commit = true commit = true
commit_args = "" commit_args = ""
current_version = "5.3.1" current_version = "5.4.2"
ignore_missing_version = false ignore_missing_version = false
message = "chore(deps): bump version from {current_version} to {new_version}" message = "chore(deps): bump version from {current_version} to {new_version}"
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?' parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
@ -137,13 +137,6 @@ replace = '''<!-- start changelog -->
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)''' ## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)'''
search = "<!-- start changelog -->" search = "<!-- start changelog -->"
[[tool.bumpversion.files]]
filename = "uv.lock"
replace = '''name = "manim-slides"
version = "{new_version}"'''
search = '''name = "manim-slides"
version = "{current_version}"'''
[tool.codespell] [tool.codespell]
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US" builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
check-hidden = true check-hidden = true

View File

@ -86,6 +86,15 @@ class TestBaseSlide:
assert base_slide.wait_time_between_slides == 0.0 assert base_slide.wait_time_between_slides == 0.0
def test_skip_animations(self, base_slide: BaseSlide) -> None:
assert not base_slide._skip_animations
def test_start_and_stop_skip_animations(self, base_slide: BaseSlide) -> None:
base_slide.start_skip_animations()
assert base_slide._skip_animations
base_slide.stop_skip_animations()
assert not base_slide._skip_animations
def test_play(self) -> None: def test_play(self) -> None:
pass # This method should be tested in test_slide.py pass # This method should be tested in test_slide.py

View File

@ -1,6 +1,10 @@
import contextlib
import os
import random import random
import shutil import shutil
import sys import sys
import tempfile
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 +21,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
@ -65,7 +70,9 @@ Slide = Union[CESlide, _GLSlide, CEGLSlide]
reason="See https://github.com/3b1b/manim/issues/2263.", reason="See https://github.com/3b1b/manim/issues/2263.",
), ),
), ),
"--CE --renderer=opengl",
], ],
ids=("CE", "GL", "CE(GL)"),
) )
def test_render_basic_slide( def test_render_basic_slide(
renderer: str, renderer: str,
@ -78,7 +85,7 @@ def test_render_basic_slide(
with runner.isolated_filesystem() as tmp_dir: with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir) shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke( results = runner.invoke(
render, [renderer, str(slides_file), "BasicSlide", "-ql"] render, [*renderer.split(" "), str(slides_file), "BasicSlide", "-ql"]
) )
assert results.exit_code == 0, results assert results.exit_code == 0, results
@ -229,8 +236,22 @@ def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct() init_slide(cls).construct()
@contextlib.contextmanager
def tmp_cwd() -> Iterator[str]:
cwd = os.getcwd()
tmp_dir = tempfile.mkdtemp()
os.chdir(tmp_dir)
try:
yield tmp_dir
finally:
os.chdir(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 +500,75 @@ 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))
class Baz(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.start_skip_animations()
self.next_slide()
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert self._base_slide_config.skip_animations
self.next_slide()
assert self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
self.stop_skip_animations()
with tmp_cwd() as tmp_dir:
init_slide(Foo).render()
init_slide(Bar).render()
init_slide(Baz).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
slide_file = slides_folder / "Baz.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 1
def test_canvas(self) -> None: def test_canvas(self) -> None:
@assert_constructs @assert_constructs
class _(CESlide): class _(CESlide):

989
uv.lock generated

File diff suppressed because it is too large Load Diff