Compare commits

..

8 Commits

Author SHA1 Message Date
2ba9b734a3 chore(version): bump 4.10.0 to 4.11.0 2023-03-20 16:32:24 +01:00
49c4a10453 fix(lib): prevent calling child method (#163)
Change `pause` to not call eventual child's `next_slide` method
2023-03-20 16:24:46 +01:00
8c578d2577 fix(cli): do not terminate slides early (#162)
* fix(cli): do not terminate slides early

When a slide is replayed (either normally or reversed), its state must be reset.

Closes #161

* [pre-commit.ci] auto fixes from pre-commit.com hooks

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-20 16:24:36 +01:00
2a327c470b feat(cli): auto detect resolution (#158)
* feat(cli): auto detect resolution

The `present` command will now read by default the resolution of each presentation, and only change it if specified by the user.

This PR also fixes bugs introduced by #156 and previous PRs, where the transition between two presentation was not correct...

* fix(lib): better to test if not None
2023-03-16 15:41:31 +01:00
04dcf530f5 [pre-commit.ci] pre-commit autoupdate (#157)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.254 → v0.0.255](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.254...v0.0.255)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-14 14:48:13 +01:00
9a573f29f1 feat(cli): add start at index options (#156)
* wip: start at index

* feat(cli): add start at index options

* fix(ci): correct callback

* chore(ci): fix type hint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

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

* fix(cli): typo in variable name...

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-11 18:05:07 +01:00
02f425f536 feat(render): support skipping animations (#155)
* feat(render): support skipping animations

Manim Slides now supports when Manim or ManimGL skip rendering some animations. All non-public attributes or methods are now named with leading __

* [pre-commit.ci] auto fixes from pre-commit.com hooks

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-10 17:49:37 +01:00
149b12fd01 chore(lib): raise error if skipping
Temporary error before implementing this
2023-03-09 17:29:26 +01:00
9 changed files with 354 additions and 119 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.10.0
current_version = 4.11.0
commit = True
message = chore(version): bump {current_version} to {new_version}

View File

@ -24,11 +24,11 @@ repos:
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.254
rev: v0.0.255
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
rev: v1.1.1
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

View File

@ -32,6 +32,25 @@ class BasicExample(Slide):
self.next_slide() # Waits user to press continue to go to the next slide
class MultipleAnimationsInLastSlide(Slide):
"""This is used to check against solution for issue #161."""
def construct(self):
circle = Circle(color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
self.next_slide()
class TestFileTooLong(Slide):
"""This is used to check against solution for issue #123."""

View File

@ -1 +1 @@
__version__ = "4.10.0"
__version__ = "4.11.0"

View File

@ -5,9 +5,9 @@ import subprocess
import tempfile
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional, Set, Union
from typing import Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel, FilePath, root_validator, validator
from pydantic import BaseModel, FilePath, PositiveInt, root_validator, validator
from PySide6.QtCore import Qt
from .defaults import FFMPEG_BIN
@ -20,7 +20,7 @@ def merge_basenames(files: List[FilePath]) -> Path:
"""
logger.info(f"Generating a new filename for animations: {files}")
dirname = files[0].parent
dirname: Path = files[0].parent
ext = files[0].suffix
basenames = (file.stem for file in files)
@ -31,7 +31,7 @@ def merge_basenames(files: List[FilePath]) -> Path:
# https://github.com/jeertmans/manim-slides/issues/123
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
return dirname / (basename + ext)
return dirname.joinpath(basename + ext)
class Key(BaseModel): # type: ignore
@ -149,13 +149,14 @@ class SlideConfig(BaseModel): # type: ignore
class PresentationConfig(BaseModel): # type: ignore
slides: List[SlideConfig]
files: List[FilePath]
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
@root_validator
def animation_indices_match_files(
cls, values: Dict[str, Union[List[SlideConfig], List[FilePath]]]
) -> Dict[str, Union[List[SlideConfig], List[FilePath]]]:
files = values.get("files")
slides = values.get("slides")
files: List[FilePath] = values.get("files") # type: ignore
slides: List[SlideConfig] = values.get("slides") # type: ignore
if files is None or slides is None:
return values
@ -163,7 +164,7 @@ class PresentationConfig(BaseModel): # type: ignore
n_files = len(files)
for slide in slides:
if slide.end_animation > n_files: # type: ignore
if slide.end_animation > n_files:
raise ValueError(
f"The following slide's contains animations not listed in files {files}: {slide}"
)

View File

@ -24,7 +24,7 @@ from .logger import logger
from .present import get_scenes_presentation_config
def open_with_default(file: Path):
def open_with_default(file: Path) -> None:
system = platform.system()
if system == "Darwin":
subprocess.call(("open", str(file)))
@ -66,7 +66,7 @@ class Converter(BaseModel): # type: ignore
An empty string is returned if no template is used."""
return ""
def open(self, file: Path) -> bool:
def open(self, file: Path) -> Any:
"""Opens a file, generated with converter, using appropriate application."""
raise NotImplementedError
@ -376,7 +376,7 @@ class PowerPoint(Converter):
use_enum_values = True
extra = "forbid"
def open(self, file: Path) -> bool:
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
@ -389,7 +389,9 @@ class PowerPoint(Converter):
# From GitHub issue comment:
# - https://github.com/scanny/python-pptx/issues/427#issuecomment-856724440
def auto_play_media(media: pptx.shapes.picture.Movie, loop: bool = False):
def auto_play_media(
media: pptx.shapes.picture.Movie, loop: bool = False
) -> None:
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
el_cnt = xpath(
media.element.getparent().getparent().getparent(),
@ -463,7 +465,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
ctx.exit()
return click.option(
return click.option( # type: ignore
"--show-config",
is_flag=True,
help="Show supported options for given format and exit.",
@ -491,7 +493,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
ctx.exit()
return click.option(
return click.option( # type: ignore
"--show-template",
is_flag=True,
help="Show the template (currently) used for a given conversion format and exit.",

View File

@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import click
import cv2
import numpy as np
from click import Context, Parameter
from pydantic import ValidationError
from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent
@ -70,10 +71,9 @@ class Presentation:
"""Creates presentation from a configuration object."""
def __init__(self, config: PresentationConfig) -> None:
self.slides: List[SlideConfig] = config.slides
self.files: List[str] = config.files
self.config = config
self.current_slide_index: int = 0
self.__current_slide_index: int = 0
self.current_animation: int = self.current_slide.start_animation
self.current_file: str = ""
@ -87,6 +87,66 @@ class Presentation:
self.reset()
def __len__(self) -> int:
return len(self.slides)
@property
def slides(self) -> List[SlideConfig]:
"""Returns the list of slides."""
return self.config.slides
@property
def files(self) -> List[Path]:
"""Returns the list of animation files."""
return self.config.files
@property
def resolution(self) -> Tuple[int, int]:
"""Returns the resolution."""
return self.config.resolution
@property
def current_slide_index(self) -> int:
return self.__current_slide_index
@current_slide_index.setter
def current_slide_index(self, value: Optional[int]) -> None:
if value is not None:
if -len(self) <= value < len(self):
self.__current_slide_index = value
self.current_animation = self.current_slide.start_animation
logger.debug(f"Set current slide index to {value}")
else:
logger.error(
f"Could not load slide number {value}, playing first slide instead."
)
def set_current_animation_and_update_slide_number(
self, value: Optional[int]
) -> None:
if value is not None:
n_files = len(self.files)
if -n_files <= value < n_files:
if value < 0:
value += n_files
for i, slide in enumerate(self.slides):
if value < slide.end_animation:
self.current_slide_index = i
self.current_animation = value
logger.debug(f"Playing animation {value}, at slide index {i}")
return
assert (
False
), f"An error occurred when setting the current animation to {value}, please create an issue on GitHub!"
else:
logger.error(
f"Could not load animation number {value}, playing first animation instead."
)
@property
def current_slide(self) -> SlideConfig:
"""Returns currently playing slide."""
@ -119,7 +179,7 @@ class Presentation:
self.release_cap()
file: str = self.files[animation]
file: str = str(self.files[animation])
if self.reverse:
file = "{}_reversed{}".format(*os.path.splitext(file))
@ -138,7 +198,9 @@ class Presentation:
def rewind_current_slide(self) -> None:
"""Rewinds current slide to first frame."""
logger.debug("Rewinding curring slide")
logger.debug("Rewinding current slide")
self.current_slide.terminated = False
if self.reverse:
self.current_animation = self.current_slide.end_animation - 1
else:
@ -176,9 +238,10 @@ class Presentation:
def load_previous_slide(self) -> None:
"""Loads previous slide."""
logger.debug("Loading previous slide")
logger.debug(f"Loading previous slide, current is {self.current_slide_index}")
self.cancel_reverse()
self.current_slide_index = max(0, self.current_slide_index - 1)
logger.debug(f"Loading slide index {self.current_slide_index}")
self.rewind_current_slide()
@property
@ -189,7 +252,8 @@ class Presentation:
logger.warn(
f"Something is wrong with video file {self.current_file}, as the fps returned by frame {self.current_frame_number} is 0"
)
return max(fps, 1) # TODO: understand why we sometimes get 0 fps
# TODO: understand why we sometimes get 0 fps
return max(fps, 1) # type: ignore
def reset(self) -> None:
"""Rests current presentation."""
@ -217,7 +281,7 @@ class Presentation:
return self.current_animation + 1
@property
def is_last_animation(self) -> int:
def is_last_animation(self) -> bool:
"""Returns True if current animation is the last one of current slide."""
if self.reverse:
return self.current_animation == self.current_slide.start_animation
@ -280,16 +344,20 @@ class Display(QThread): # type: ignore
change_video_signal = Signal(np.ndarray)
change_info_signal = Signal(dict)
change_presentation_sigal = Signal()
finished = Signal()
def __init__(
self,
presentations: List[PresentationConfig],
presentations: List[Presentation],
config: Config = DEFAULT_CONFIG,
start_paused: bool = False,
skip_all: bool = False,
record_to: Optional[str] = None,
exit_after_last_slide: bool = False,
start_at_scene_number: Optional[int] = None,
start_at_slide_number: Optional[int] = None,
start_at_animation_number: Optional[int] = None,
) -> None:
super().__init__()
self.presentations = presentations
@ -301,17 +369,48 @@ class Display(QThread): # type: ignore
self.state = State.PLAYING
self.lastframe: Optional[np.ndarray] = None
self.current_presentation_index = 0
self.__current_presentation_index = 0
self.current_presentation_index = start_at_scene_number # type: ignore
self.current_presentation.current_slide_index = start_at_slide_number # type: ignore
self.current_presentation.set_current_animation_and_update_slide_number(
start_at_animation_number
)
self.run_flag = True
self.key = -1
self.exit_after_last_slide = exit_after_last_slide
def __len__(self) -> int:
return len(self.presentations)
@property
def current_presentation_index(self) -> int:
return self.__current_presentation_index
@current_presentation_index.setter
def current_presentation_index(self, value: Optional[int]) -> None:
if value is not None:
if -len(self) <= value < len(self):
self.__current_presentation_index = value
self.current_presentation.release_cap()
self.change_presentation_sigal.emit()
else:
logger.error(
f"Could not load scene number {value}, playing first scene instead."
)
@property
def current_presentation(self) -> Presentation:
"""Returns the current presentation."""
return self.presentations[self.current_presentation_index]
@property
def current_resolution(self) -> Tuple[int, int]:
"""Returns the resolution of the current presentation."""
return self.current_presentation.resolution
def run(self) -> None:
"""Runs a series of presentations until end or exit."""
while self.run_flag:
@ -346,7 +445,7 @@ class Display(QThread): # type: ignore
if self.record_to is not None:
self.record_movie()
logger.debug("Closing video thread gracully and exiting")
logger.debug("Closing video thread gracefully and exiting")
self.finished.emit()
def record_movie(self) -> None:
@ -520,7 +619,6 @@ class App(QWidget): # type: ignore
*args: Any,
config: Config = DEFAULT_CONFIG,
fullscreen: bool = False,
resolution: Tuple[int, int] = (1980, 1080),
hide_mouse: bool = False,
aspect_ratio: AspectRatio = AspectRatio.auto,
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
@ -532,7 +630,12 @@ class App(QWidget): # type: ignore
self.setWindowTitle(WINDOW_NAME)
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.display_width, self.display_height = resolution
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
self.display_width, self.display_height = self.thread.current_resolution
self.aspect_ratio = aspect_ratio
self.resize_mode = resize_mode
self.hide_mouse = hide_mouse
@ -552,9 +655,6 @@ class App(QWidget): # type: ignore
self.label.setPixmap(self.pixmap)
self.label.setMinimumSize(1, 1)
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
# create the info dialog
self.info = Info()
self.info.show()
@ -568,6 +668,7 @@ class App(QWidget): # type: ignore
# connect signals
self.thread.change_video_signal.connect(self.update_image)
self.thread.change_info_signal.connect(self.info.update_info)
self.thread.change_presentation_sigal.connect(self.update_canvas)
self.thread.finished.connect(self.closeAll)
self.send_key_signal.connect(self.thread.set_key)
@ -621,6 +722,14 @@ class App(QWidget): # type: ignore
self.label.setPixmap(QPixmap.fromImage(qt_img))
@Slot()
def update_canvas(self) -> None:
"""Update the canvas when a presentation has changed."""
logger.debug("Updating canvas")
self.display_width, self.display_height = self.thread.current_resolution
if not self.isFullScreen():
self.resize(self.display_width, self.display_height)
@click.command()
@click.option(
@ -690,7 +799,7 @@ def prompt_for_scenes(folder: Path) -> List[str]:
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
return scenes
return scenes # type: ignore
except ValueError as e:
raise click.UsageError(str(e))
@ -718,6 +827,37 @@ def get_scenes_presentation_config(
return presentation_configs
def start_at_callback(
ctx: Context, param: Parameter, values: str
) -> Tuple[Optional[int], ...]:
if values == "(None, None, None)":
return (None, None, None)
def str_to_int_or_none(value: str) -> Optional[int]:
if value.lower().strip() == "":
return None
else:
try:
return int(value)
except ValueError:
raise click.BadParameter(
f"start index can only be an integer or an empty string, not `{value}`",
ctx=ctx,
param=param,
)
values_tuple = values.split(",")
n_values = len(values_tuple)
if n_values == 3:
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 3 arguments are expected but you gave {n_values}, please use commas to separate them",
ctx=ctx,
param=param,
)
@click.command()
@click.argument("scenes", nargs=-1)
@config_path_option
@ -742,9 +882,8 @@ def get_scenes_presentation_config(
"--resolution",
metavar="<WIDTH HEIGHT>",
type=(int, int),
default=(1920, 1080),
default=None,
help="Window resolution WIDTH HEIGHT used if fullscreen is not set. You may manually resize the window afterward.",
show_default=True,
)
@click.option(
"--to",
@ -789,6 +928,43 @@ def get_scenes_presentation_config(
help='Set the background color for borders when using "keep" resize mode. Can be any valid CSS color, e.g., "green", "#FF6500" or "rgba(255, 255, 0, .5)".',
show_default=True,
)
@click.option(
"--sa",
"--start-at",
"start_at",
metavar="<SCENE,SLIDE,ANIMATION>",
type=str,
callback=start_at_callback,
default=(None, None, None),
help="Start presenting at (x, y, z), equivalent to --sacn x --sasn y --saan z, and overrides values if not None.",
)
@click.option(
"--sacn",
"--start-at-scene-number",
"start_at_scene_number",
metavar="INDEX",
type=int,
default=None,
help="Start presenting at a given scene number (0 is first, -1 is last).",
)
@click.option(
"--sasn",
"--start-at-slide-number",
"start_at_slide_number",
metavar="INDEX",
type=int,
default=None,
help="Start presenting at a given slide number (0 is first, -1 is last).",
)
@click.option(
"--saan",
"--start-at-animation-number",
"start_at_animation_number",
metavar="INDEX",
type=int,
default=0,
help="Start presenting at a given animation number (0 is first, -1 is last). This conflicts with slide number since animation number is absolute to the presentation.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
@ -798,13 +974,17 @@ def present(
start_paused: bool,
fullscreen: bool,
skip_all: bool,
resolution: Tuple[int, int],
resolution: Optional[Tuple[int, int]],
record_to: Optional[Path],
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
resize_mode: str,
background_color: str,
start_at: Tuple[Optional[int], Optional[int], Optional[int]],
start_at_scene_number: Optional[int],
start_at_slide_number: Optional[int],
start_at_animation_number: Optional[int],
) -> None:
"""
Present SCENE(s), one at a time, in order.
@ -819,9 +999,15 @@ def present(
if skip_all:
exit_after_last_slide = True
presentation_configs = get_scenes_presentation_config(scenes, folder)
if resolution is not None:
for presentation_config in presentation_configs:
presentation_config.resolution = resolution
presentations = [
Presentation(presentation_config)
for presentation_config in get_scenes_presentation_config(scenes, folder)
for presentation_config in presentation_configs
]
if config_path.exists():
@ -840,6 +1026,15 @@ def present(
"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
)
if start_at[0]:
start_at_scene_number = start_at[0]
if start_at[1]:
start_at_slide_number = start_at[1]
if start_at[2]:
start_at_animation_number = start_at[2]
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides")
a = App(
@ -848,13 +1043,15 @@ def present(
start_paused=start_paused,
fullscreen=fullscreen,
skip_all=skip_all,
resolution=resolution,
record_to=record_to,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio=ASPECT_RATIO_MODES[aspect_ratio],
resize_mode=RESIZE_MODES[resize_mode],
background_color=background_color,
start_at_scene_number=start_at_scene_number,
start_at_slide_number=start_at_slide_number,
start_at_animation_number=start_at_animation_number,
)
a.show()
sys.exit(app.exec_())

View File

@ -2,7 +2,7 @@ import os
import platform
import shutil
import subprocess
from typing import Any, List, Optional
from typing import Any, List, Optional, Tuple
from warnings import warn
from tqdm import tqdm
@ -47,15 +47,23 @@ class Slide(Scene): # type:ignore
super().__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides: List[SlideConfig] = []
self.current_slide = 1
self.current_animation = 0
self.loop_start_animation: Optional[int] = None
self.pause_start_animation = 0
self.__output_folder = output_folder
self.__slides: List[SlideConfig] = []
self.__current_slide = 1
self.__current_animation = 0
self.__loop_start_animation: Optional[int] = None
self.__pause_start_animation = 0
@property
def partial_movie_files(self) -> List[str]:
def __resolution(self) -> Tuple[int, int]:
"""Returns the scene's resolution used during rendering."""
if MANIMGL:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
else:
return config["pixel_width"], config["pixel_height"]
@property
def __partial_movie_files(self) -> List[str]:
"""Returns a list of partial movie files, a.k.a animations."""
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
@ -64,34 +72,41 @@ class Slide(Scene): # type:ignore
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
return get_sorted_integer_files( # type: ignore
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
return self.renderer.file_writer.partial_movie_files # type: ignore
@property
def show_progress_bar(self) -> bool:
def __show_progress_bar(self) -> bool:
"""Returns True if progress bar should be displayed."""
if MANIMGL:
return getattr(super(Scene, self), "show_progress_bar", True)
return getattr(self, "show_progress_bar", True)
else:
return config["progress_bar"] != "none"
return config["progress_bar"] != "none" # type: ignore
@property
def leave_progress_bar(self) -> bool:
def __leave_progress_bar(self) -> bool:
"""Returns True if progress bar should be left after completed."""
if MANIMGL:
return getattr(super(Scene, self), "leave_progress_bars", False)
return getattr(self, "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
return config["progress_bar"] == "leave" # type: ignore
@property
def __start_at_animation_number(self) -> Optional[int]:
if MANIMGL:
return getattr(self, "start_at_animation_number", None)
else:
return config["from_animation_number"] # type: ignore
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overloads `self.play` and increment animation count."""
super().play(*args, **kwargs)
self.current_animation += 1
self.__current_animation += 1
def next_slide(self):
def next_slide(self) -> None:
"""
Creates a new slide with previous animations.
@ -133,19 +148,19 @@ class Slide(Scene): # type:ignore
self.play(FadeOut(text))
"""
assert (
self.loop_start_animation is None
self.__loop_start_animation is None
), "You cannot call `self.next_slide()` inside a loop"
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.slide,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
start_animation=self.__pause_start_animation,
end_animation=self.__current_animation,
number=self.__current_slide,
)
)
self.current_slide += 1
self.pause_start_animation = self.current_animation
self.__current_slide += 1
self.__pause_start_animation = self.__current_animation
def pause(self) -> None:
"""
@ -159,24 +174,24 @@ class Slide(Scene): # type:ignore
DeprecationWarning,
stacklevel=2,
)
self.next_slide()
Slide.next_slide(self)
def add_last_slide(self) -> None:
def __add_last_slide(self) -> None:
"""Adds a 'last' slide to the end of slides."""
if (
len(self.slides) > 0
and self.current_animation == self.slides[-1].end_animation
len(self.__slides) > 0
and self.__current_animation == self.__slides[-1].end_animation
):
self.slides[-1].type = SlideType.last
self.__slides[-1].type = SlideType.last
return
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.last,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
start_animation=self.__pause_start_animation,
end_animation=self.__current_animation,
number=self.__current_slide,
)
)
@ -207,42 +222,42 @@ class Slide(Scene): # type:ignore
self.end_loop()
"""
assert self.loop_start_animation is None, "You cannot nest loops"
self.loop_start_animation = self.current_animation
assert self.__loop_start_animation is None, "You cannot nest loops"
self.__loop_start_animation = self.__current_animation
def end_loop(self) -> None:
"""Ends an existing loop. See :func:`start_loop` for more details."""
assert (
self.loop_start_animation is not None
self.__loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.loop,
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
start_animation=self.__loop_start_animation,
end_animation=self.__current_animation,
number=self.__current_slide,
)
)
self.current_slide += 1
self.loop_start_animation = None
self.pause_start_animation = self.current_animation
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:
"""
Saves slides, optionally using cached files.
Note that cached files only work with Manim.
"""
self.add_last_slide()
self.__add_last_slide()
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
if not os.path.exists(self.__output_folder):
os.mkdir(self.__output_folder)
files_folder = os.path.join(self.output_folder, "files")
files_folder = os.path.join(self.__output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
scene_name = type(self).__name__
scene_name = str(self)
scene_files_folder = os.path.join(files_folder, scene_name)
old_animation_files = set()
@ -257,12 +272,18 @@ class Slide(Scene): # type:ignore
files = []
for src_file in tqdm(
self.partial_movie_files,
self.__partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=self.leave_progress_bar,
leave=self.__leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self.show_progress_bar,
disable=not self.__show_progress_bar,
):
if src_file is None and not MANIMGL:
# This happens if rendering with -na,b (manim only)
# where animations not in [a,b] will be skipped
# but animations before a will have a None src_file
continue
filename = os.path.basename(src_file)
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
@ -282,14 +303,27 @@ class Slide(Scene): # type:ignore
files.append(dst_file)
if offset := self.__start_at_animation_number:
self.__slides = [
slide for slide in self.__slides if slide.end_animation > offset
]
for slide in self.__slides:
slide.start_animation -= offset
slide.end_animation -= offset
logger.info(
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
)
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
slide_path = os.path.join(self.__output_folder, "%s.json" % (scene_name,))
with open(slide_path, "w") as f:
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
f.write(
PresentationConfig(
slides=self.__slides, files=files, resolution=self.__resolution
).json(indent=2)
)
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
@ -298,11 +332,11 @@ class Slide(Scene): # type:ignore
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
self.__save_slides(use_cache=False)
def render(self, *args: Any, **kwargs: Any) -> None:
"""MANIM render"""
# We need to disable the caching limit since we rely on intermidiate files
# We need to disable the caching limit since we rely on intermediate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
@ -310,7 +344,7 @@ class Slide(Scene): # type:ignore
config["max_files_cached"] = max_files_cached
self.save_slides()
self.__save_slides()
class ThreeDSlide(Slide, ThreeDScene): # type: ignore

View File

@ -10,28 +10,10 @@ profile = "black"
py_version = 38
[tool.mypy]
check-untyped-defs = true
# Disallow dynamic typing
disallow-any-generics = true
disallow-incomplete-defs = true
disallow-subclassing-any = true
# Disallow untyped definitions and calls
disallow-untyped-defs = true
ignore-missing-imports = true
install-types = true
# None and optional handling
no-implicit-optional = true
no-warn-return-any = true
non-interactive = true
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
# Strict equality
strict-equality = true
warn-no-return = true
warn-redundant-casts = true
# Config file
warn-unused-configs = true
# Configuring warnings
warn-unused-ignores = true
strict = true
[tool.poetry]
authors = [
@ -61,7 +43,7 @@ packages = [
]
readme = "README.md"
repository = "https://github.com/jeertmans/manim-slides"
version = "4.10.0"
version = "4.11.0"
[tool.poetry.dependencies]
click = "^8.1.3"