Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat(cli): new player
  • Loading branch information
jeertmans committed Aug 21, 2023
commit a69fdaac1c0aab4da2d1464e118678addad1209f
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
poetry install --with test

- name: Run pytest
run: poetry run pytest -x
run: poetry run pytest -x -n auto

build-examples:
strategy:
Expand Down
95 changes: 59 additions & 36 deletions manim_slides/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import json
import shutil
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Set, Tuple

import rtoml
from pydantic import (
BaseModel,
Field,
FilePath,
PositiveInt,
PrivateAttr,
field_validator,
model_validator,
)
Expand All @@ -18,13 +18,31 @@

from .logger import logger

Receiver = Callable[..., Any]

class Key(BaseModel): # type: ignore

class Signal(BaseModel): # type: ignore[misc]
__receivers: List[Receiver] = PrivateAttr(default_factory=list)

def connect(self, receiver: Receiver) -> None:
self.__receivers.append(receiver)

def disconnect(self, receiver: Receiver) -> None:
self.__receivers.remove(receiver)

def emit(self, *args: Any) -> None:
for receiver in self.__receivers:
receiver(*args)


class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""

ids: List[PositiveInt] = Field(unique=True)
name: Optional[str] = None

__signal: Signal = PrivateAttr(default_factory=Signal)

@field_validator("ids")
@classmethod
def ids_is_non_empty_set(cls, ids: Set[Any]) -> Set[Any]:
Expand All @@ -43,14 +61,22 @@ def match(self, key_id: int) -> bool:

return m

@property
def signal(self) -> Signal:
return self.__signal

def connect(self, function: Receiver) -> None:
self.__signal.connect(function)

class Keys(BaseModel): # type: ignore

class Keys(BaseModel): # type: ignore[misc]
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
CONTINUE: Key = Key(ids=[Qt.Key_Right], name="CONTINUE / NEXT")
BACK: Key = Key(ids=[Qt.Key_Left], name="BACK")
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
REWIND: Key = Key(ids=[Qt.Key_R], name="REWIND")
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
NEXT: Key = Key(ids=[Qt.Key_Right], name="NEXT")
PREVIOUS: Key = Key(ids=[Qt.Key_Left], name="PREVIOUS")
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
REPLAY: Key = Key(ids=[Qt.Key_R], name="REPLAY")
FULL_SCREEN: Key = Key(ids=[Qt.Key_F], name="TOGGLE FULL SCREEN")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")

@model_validator(mode="before")
Expand All @@ -74,8 +100,21 @@ def merge_with(self, other: "Keys") -> "Keys":

return self

def dispatch_key_function(self) -> Callable[[PositiveInt], None]:
_dispatch = {}

class Config(BaseModel): # type: ignore
for _, key in self:
for _id in key.ids:
_dispatch[_id] = key.signal

def dispatch(key: PositiveInt) -> None:
if signal := _dispatch.get(key, None):
signal.emit()

return dispatch


class Config(BaseModel): # type: ignore[misc]
"""General Manim Slides config"""

keys: Keys = Keys()
Expand All @@ -94,16 +133,10 @@ def merge_with(self, other: "Config") -> "Config":
return self


class SlideType(str, Enum):
slide = "slide"
loop = "loop"
last = "last"


class PreSlideConfig(BaseModel): # type: ignore
type: SlideType
start_animation: int
end_animation: int
loop: bool = False

@field_validator("start_animation", "end_animation")
@classmethod
Expand All @@ -112,12 +145,12 @@ def index_is_posint(cls, v: int) -> int:
raise ValueError("Animation index (start or end) cannot be negative")
return v

@model_validator(mode="before")
@model_validator(mode="after")
def start_animation_is_before_end(
cls, values: Dict[str, Union[SlideType, int, bool]]
) -> Dict[str, Union[SlideType, int, bool]]:
if values["start_animation"] >= values["end_animation"]: # type: ignore
if values["start_animation"] == values["end_animation"] == 0:
cls, pre_slide_config: "PreSlideConfig"
) -> "PreSlideConfig":
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
)
Expand All @@ -126,36 +159,26 @@ def start_animation_is_before_end(
"Start animation index must be strictly lower than end animation index"
)

return values
return pre_slide_config

@property
def slides_slice(self) -> slice:
return slice(self.start_animation, self.end_animation)


class SlideConfig(BaseModel): # type: ignore
type: SlideType
class SlideConfig(BaseModel): # type: ignore[misc]
file: FilePath
rev_file: FilePath
terminated: bool = Field(False, exclude=True)
loop: bool = False

@classmethod
def from_pre_slide_config_and_files(
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
) -> "SlideConfig":
return cls(type=pre_slide_config.type, file=file, rev_file=rev_file)

def is_slide(self) -> bool:
return self.type == SlideType.slide

def is_loop(self) -> bool:
return self.type == SlideType.loop

def is_last(self) -> bool:
return self.type == SlideType.last
return cls(file=file, rev_file=rev_file, loop=pre_slide_config.loop)


class PresentationConfig(BaseModel): # type: ignore
class PresentationConfig(BaseModel): # type: ignore[misc]
slides: List[SlideConfig] = Field(min_length=1)
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black"
Expand Down
2 changes: 1 addition & 1 deletion manim_slides/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def get_sections_iter(self, assets_dir: Path) -> Generator[str, None, None]:
# Later, this might be useful to only mute the first video, or to make it optional.
# Read more about this:
# https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_and_autoplay_blocking
if slide_config.is_loop():
if slide_config.loop:
yield f'<section data-background-size={self.background_size.value} data-background-color="{presentation_config.background_color}" data-background-video="{file}" data-background-video-muted data-background-video-loop></section>'
else:
yield f'<section data-background-size={self.background_size.value} data-background-color="{presentation_config.background_color}" data-background-video="{file}" data-background-video-muted></section>'
Expand Down
Loading