mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-21 20:46:01 +08:00
feat: large "present" feature refactoring (#15)
* fix(cli): previous presentation starts last slide This changes the previous behavior that was such that going to previous presentation (scene) would start it from zero. * fix(README): remove non-working svgs Seems like GitHub does not support that * wip: onto a cleaner parser, and more robust presentation * fix(README): remove non-working svgs Seems like GitHub does not support that * wip: onto a cleaner parser, and more robust presentation * wip: some progress * fix: this is kinda working now * fix: repeated reversed slide * fix: import __version__ without importing package * fix: typing list -> List * chore(README): update features
This commit is contained in:
18
README.md
18
README.md
@ -113,14 +113,14 @@ manim-slides Example
|
|||||||
|
|
||||||
The default key bindings to control the presentation are:
|
The default key bindings to control the presentation are:
|
||||||
|
|
||||||
| Keybinding | Action | Icon |
|
| Keybinding | Action |
|
||||||
|:-----------:|:------------------------:|:----:|
|
|:-----------:|:------------------------:|
|
||||||
| Right Arrow | Continue/Next Slide | <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z"/></svg> |
|
| Right Arrow | Continue/Next Slide |
|
||||||
| Left Arrow | Previous Slide | <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M267.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29V96c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160L64 241V96c0-17.7-14.3-32-32-32S0 78.3 0 96V416c0 17.7 14.3 32 32 32s32-14.3 32-32V271l11.5 9.6 192 160z"/></svg> |
|
| Left Arrow | Previous Slide |
|
||||||
| R | Replay Current Slide | <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"/></svg> |
|
| R | Replay Current Slide |
|
||||||
| V | Reverse Current Slide | <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M459.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29V96c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4L288 214.3V256v41.7L459.5 440.6zM256 352V256 128 96c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160C4.2 237.5 0 246.5 0 256s4.2 18.5 11.5 24.6l192 160c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29V352z"/></svg> |
|
| V | Reverse Current Slide |
|
||||||
| Spacebar | Play/Pause | <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg> <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"/></svg> |
|
| Spacebar | Play/Pause |
|
||||||
| Q | Quit | <svg height="25px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"/></svg> |
|
| Q | Quit |
|
||||||
|
|
||||||
You can run the **configuration wizard** to change those key bindings:
|
You can run the **configuration wizard** to change those key bindings:
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ Below is a non-exhaustive list of features:
|
|||||||
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
|
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
|
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
|
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
| Documented code | WIP | :heavy_multiplication_x: |
|
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
|
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "3.2.4"
|
__version__ = "4-rc.1"
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
from typing import Optional, Set
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
from pydantic import BaseModel, root_validator, validator
|
from pydantic import BaseModel, FilePath, root_validator, validator
|
||||||
|
|
||||||
from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
|
from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
|
||||||
|
|
||||||
|
|
||||||
class Key(BaseModel):
|
class Key(BaseModel):
|
||||||
|
"""Represents a list of key codes, with optionally a name."""
|
||||||
|
|
||||||
ids: Set[int]
|
ids: Set[int]
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
||||||
@ -20,6 +24,8 @@ class Key(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
|
"""General Manim Slides config"""
|
||||||
|
|
||||||
QUIT: Key = Key(ids=[ord("q")], name="QUIT")
|
QUIT: Key = Key(ids=[ord("q")], name="QUIT")
|
||||||
CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT")
|
CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT")
|
||||||
BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK")
|
BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK")
|
||||||
@ -47,3 +53,83 @@ class Config(BaseModel):
|
|||||||
key.name = other_key.name or key.name
|
key.name = other_key.name or key.name
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class SlideType(str, Enum):
|
||||||
|
slide = "slide"
|
||||||
|
loop = "loop"
|
||||||
|
last = "last"
|
||||||
|
|
||||||
|
|
||||||
|
class SlideConfig(BaseModel):
|
||||||
|
type: SlideType
|
||||||
|
start_animation: int
|
||||||
|
end_animation: int
|
||||||
|
number: int
|
||||||
|
terminated: bool = False
|
||||||
|
|
||||||
|
@validator("start_animation", "end_animation")
|
||||||
|
def index_is_posint(cls, v: int):
|
||||||
|
if v < 0:
|
||||||
|
raise ValueError("Animation index (start or end) cannot be negative")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator("number")
|
||||||
|
def number_is_strictly_posint(cls, v: int):
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Slide number cannot be negative or zero")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@root_validator
|
||||||
|
def start_animation_is_before_end(cls, values):
|
||||||
|
if values["start_animation"] >= values["end_animation"]:
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"Start animation index must be strictly lower than end animation index"
|
||||||
|
)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def is_slide(self):
|
||||||
|
return self.type == SlideType.slide
|
||||||
|
|
||||||
|
def is_loop(self):
|
||||||
|
return self.type == SlideType.loop
|
||||||
|
|
||||||
|
def is_last(self):
|
||||||
|
return self.type == SlideType.last
|
||||||
|
|
||||||
|
|
||||||
|
class PresentationConfig(BaseModel):
|
||||||
|
slides: List[SlideConfig]
|
||||||
|
files: List[str]
|
||||||
|
|
||||||
|
@validator("files", pre=True, each_item=True)
|
||||||
|
def is_file_and_exists(cls, v):
|
||||||
|
if not os.path.exists(v):
|
||||||
|
raise ValueError(
|
||||||
|
f"Animation file {v} does not exist. Are you in the right directory?"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.isfile(v):
|
||||||
|
raise ValueError(f"Animation file {v} is not a file")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
@root_validator
|
||||||
|
def animation_indices_match_files(cls, values):
|
||||||
|
files = values.get("files")
|
||||||
|
slides = values.get("slides")
|
||||||
|
|
||||||
|
if files is None or slides is None:
|
||||||
|
return values
|
||||||
|
|
||||||
|
n_files = len(files)
|
||||||
|
|
||||||
|
for slide in slides:
|
||||||
|
if slide.end_animation > n_files:
|
||||||
|
raise ValueError(
|
||||||
|
f"The following slide's contains animations not listed in files {files}: {slide}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return values
|
||||||
|
@ -5,6 +5,7 @@ import platform
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from enum import IntEnum, auto, unique
|
from enum import IntEnum, auto, unique
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
import ctypes
|
import ctypes
|
||||||
@ -12,132 +13,197 @@ if platform.system() == "Windows":
|
|||||||
import click
|
import click
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from .commons import config_path_option
|
from .commons import config_path_option
|
||||||
from .config import Config
|
from .config import Config, PresentationConfig, SlideConfig, SlideType
|
||||||
from .defaults import CONFIG_PATH, FOLDER_PATH
|
from .defaults import CONFIG_PATH, FOLDER_PATH
|
||||||
from .slide import reverse_video_path
|
|
||||||
|
|
||||||
WINDOW_NAME = "Manim Slides"
|
WINDOW_NAME = "Manim Slides"
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class State(IntEnum):
|
class State(IntEnum):
|
||||||
|
"""Represents all possible states of a slide presentation."""
|
||||||
|
|
||||||
PLAYING = auto()
|
PLAYING = auto()
|
||||||
PAUSED = auto()
|
PAUSED = auto()
|
||||||
WAIT = auto()
|
WAIT = auto()
|
||||||
END = auto()
|
END = auto()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name.capitalize()
|
return self.name.capitalize()
|
||||||
|
|
||||||
|
|
||||||
def now() -> int:
|
def now() -> int:
|
||||||
|
"""Returns time.time() in seconds."""
|
||||||
return round(time.time() * 1000)
|
return round(time.time() * 1000)
|
||||||
|
|
||||||
|
|
||||||
def fix_time(x: float) -> float:
|
def fix_time(t: float) -> float:
|
||||||
return x if x > 0 else 1
|
"""Clips time t such that it is always positive."""
|
||||||
|
return t if t > 0 else 1
|
||||||
|
|
||||||
|
|
||||||
class Presentation:
|
class Presentation:
|
||||||
def __init__(self, config, last_frame_next: bool = False):
|
"""Creates presentation from a configuration object."""
|
||||||
self.last_frame_next = last_frame_next
|
|
||||||
self.slides = config["slides"]
|
def __init__(self, config: PresentationConfig):
|
||||||
self.files = [path for path in config["files"]]
|
self.slides: List[SlideConfig] = config.slides
|
||||||
|
self.files: List[str] = config.files
|
||||||
|
|
||||||
|
self.current_slide_index = 0
|
||||||
|
self.current_animation = self.current_slide.start_animation
|
||||||
|
|
||||||
|
self.loaded_animation_cap = -1
|
||||||
|
self.cap = None # cap = cv2.VideoCapture
|
||||||
|
|
||||||
self.reverse = False
|
self.reverse = False
|
||||||
self.reversed_slide = -1
|
self.reversed_animation = -1
|
||||||
|
|
||||||
self.lastframe = []
|
self.lastframe = None
|
||||||
|
|
||||||
self.caps = [None for _ in self.files]
|
|
||||||
self.reset()
|
self.reset()
|
||||||
self.add_last_slide()
|
self.add_last_slide()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_slide(self) -> SlideConfig:
|
||||||
|
"""Returns currently playing slide."""
|
||||||
|
return self.slides[self.current_slide_index]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def first_slide(self) -> SlideConfig:
|
||||||
|
"""Returns first slide."""
|
||||||
|
return self.slides[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_slide(self) -> SlideConfig:
|
||||||
|
"""Returns last slide."""
|
||||||
|
return self.slides[-1]
|
||||||
|
|
||||||
|
def release_cap(self):
|
||||||
|
"""Releases current Video Capture, if existing."""
|
||||||
|
if not self.cap is None:
|
||||||
|
self.cap.release()
|
||||||
|
|
||||||
|
self.loaded_animation_cap = -1
|
||||||
|
|
||||||
|
def load_animation_cap(self, animation: int):
|
||||||
|
"""Loads video file of given animation."""
|
||||||
|
# We must load a new VideoCapture file if:
|
||||||
|
if (self.loaded_animation_cap != animation) or (
|
||||||
|
self.reverse and self.reversed_animation != animation
|
||||||
|
): # cap already loaded
|
||||||
|
|
||||||
|
self.release_cap()
|
||||||
|
|
||||||
|
file = self.files[animation]
|
||||||
|
|
||||||
|
if self.reverse:
|
||||||
|
file = "{}_reversed{}".format(*os.path.splitext(file))
|
||||||
|
self.reversed_animation = animation
|
||||||
|
|
||||||
|
self.cap = cv2.VideoCapture(file)
|
||||||
|
self.loaded_animation_cap = animation
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_cap(self) -> cv2.VideoCapture:
|
||||||
|
"""Returns current VideoCapture object."""
|
||||||
|
self.load_animation_cap(self.current_animation)
|
||||||
|
return self.cap
|
||||||
|
|
||||||
|
def rewind_current_slide(self):
|
||||||
|
"""Rewinds current slide to first frame."""
|
||||||
|
if self.reverse:
|
||||||
|
self.current_animation = self.current_slide.end_animation - 1
|
||||||
|
else:
|
||||||
|
self.current_animation = self.current_slide.start_animation
|
||||||
|
|
||||||
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||||
|
|
||||||
|
def reverse_current_slide(self):
|
||||||
|
"""Reverses current slide."""
|
||||||
|
self.reverse = True
|
||||||
|
self.rewind_current_slide()
|
||||||
|
|
||||||
|
def load_next_slide(self):
|
||||||
|
"""Loads next slide."""
|
||||||
|
if self.reverse:
|
||||||
|
self.reverse = False
|
||||||
|
self.reversed_animation = -1
|
||||||
|
self.release_cap()
|
||||||
|
self.rewind_current_slide()
|
||||||
|
elif self.current_slide.is_last():
|
||||||
|
self.current_slide.terminated = True
|
||||||
|
else:
|
||||||
|
self.current_slide_index = min(
|
||||||
|
len(self.slides) - 1, self.current_slide_index + 1
|
||||||
|
)
|
||||||
|
self.rewind_current_slide()
|
||||||
|
|
||||||
|
def load_previous_slide(self):
|
||||||
|
"""Loads previous slide."""
|
||||||
|
self.current_slide_index = max(0, self.current_slide_index - 1)
|
||||||
|
self.rewind_current_slide()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fps(self) -> int:
|
||||||
|
"""Returns the number of frames per second of the current video."""
|
||||||
|
return self.current_cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
|
||||||
def add_last_slide(self):
|
def add_last_slide(self):
|
||||||
last_slide_end = self.slides[-1]["end_animation"]
|
"""Add a 'last' slide to the end of slides."""
|
||||||
last_animation = len(self.files)
|
|
||||||
self.slides.append(
|
self.slides.append(
|
||||||
dict(
|
SlideConfig(
|
||||||
start_animation=last_slide_end,
|
start_animation=self.last_slide.end_animation,
|
||||||
end_animation=last_animation,
|
end_animation=self.last_slide.end_animation + 1,
|
||||||
type="last",
|
type=SlideType.last,
|
||||||
number=len(self.slides) + 1,
|
number=self.last_slide.number + 1,
|
||||||
terminated=False,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
"""Rests current presentation."""
|
||||||
self.current_animation = 0
|
self.current_animation = 0
|
||||||
self.load_this_cap(0)
|
self.load_animation_cap(0)
|
||||||
self.current_slide_i = 0
|
self.current_slide_index = 0
|
||||||
self.slides[-1]["terminated"] = False
|
self.slides[-1].terminated = False
|
||||||
|
|
||||||
def start_last_slide(self):
|
def load_last_slide(self):
|
||||||
self.current_animation = self.slides[-1]["start_animation"]
|
"""Loads last slide."""
|
||||||
self.load_this_cap(self.current_animation)
|
self.current_slide_index = len(self.slides) - 2
|
||||||
self.current_slide_i = len(self.slides[-1])
|
assert (
|
||||||
self.slides[-1]["terminated"] = False
|
self.current_slide_index >= 0
|
||||||
|
), "Slides should be at list of a least two elements"
|
||||||
|
self.current_animation = self.current_slide.start_animation
|
||||||
|
self.load_animation_cap(self.current_animation)
|
||||||
|
self.slides[-1].terminated = False
|
||||||
|
|
||||||
def next(self):
|
@property
|
||||||
if self.current_slide["type"] == "last":
|
def next_animation(self) -> int:
|
||||||
self.current_slide["terminated"] = True
|
"""Returns the next animation."""
|
||||||
else:
|
|
||||||
self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1)
|
|
||||||
self.rewind_slide()
|
|
||||||
|
|
||||||
def prev(self):
|
|
||||||
self.current_slide_i = max(0, self.current_slide_i - 1)
|
|
||||||
self.rewind_slide()
|
|
||||||
|
|
||||||
def reverse_slide(self):
|
|
||||||
self.rewind_slide(reverse=True)
|
|
||||||
|
|
||||||
def rewind_slide(self, reverse: bool = False):
|
|
||||||
self.reverse = reverse
|
|
||||||
self.current_animation = self.current_slide["start_animation"]
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
||||||
|
|
||||||
def load_this_cap(self, cap_number: int):
|
|
||||||
if (
|
|
||||||
self.caps[cap_number] is None
|
|
||||||
or (self.reverse and self.reversed_slide != cap_number)
|
|
||||||
or (not self.reverse and self.reversed_slide == cap_number)
|
|
||||||
):
|
|
||||||
# unload other caps
|
|
||||||
for i in range(len(self.caps)):
|
|
||||||
if not self.caps[i] is None:
|
|
||||||
self.caps[i].release()
|
|
||||||
self.caps[i] = None
|
|
||||||
# load this cap
|
|
||||||
file = self.files[cap_number]
|
|
||||||
if self.reverse:
|
if self.reverse:
|
||||||
self.reversed_slide = cap_number
|
return self.current_animation - 1
|
||||||
file = "{}_reversed{}".format(*os.path.splitext(file))
|
|
||||||
else:
|
else:
|
||||||
self.reversed_slide = -1
|
return self.current_animation + 1
|
||||||
|
|
||||||
self.caps[cap_number] = cv2.VideoCapture(file)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_slide(self):
|
def is_last_animation(self) -> int:
|
||||||
return self.slides[self.current_slide_i]
|
"""Returns True if current animation is the last one of current slide."""
|
||||||
|
if self.reverse:
|
||||||
|
return self.current_animation == self.current_slide.start_animation
|
||||||
|
else:
|
||||||
|
return self.next_animation == self.current_slide.end_animation
|
||||||
|
|
||||||
@property
|
def update_state(self, state) -> Tuple[np.ndarray, State]:
|
||||||
def current_cap(self):
|
"""
|
||||||
self.load_this_cap(self.current_animation)
|
Updates the current state given the previous one.
|
||||||
return self.caps[self.current_animation]
|
|
||||||
|
|
||||||
@property
|
It does this by reading the video information and checking if the state is still correct.
|
||||||
def fps(self):
|
It returns the frame to show (lastframe) and the new state.
|
||||||
return self.current_cap.get(cv2.CAP_PROP_FPS)
|
"""
|
||||||
|
|
||||||
# This function updates the state given the previous state.
|
|
||||||
# It does this by reading the video information and checking if the state is still correct.
|
|
||||||
# It returns the frame to show (lastframe) and the new state.
|
|
||||||
def update_state(self, state):
|
|
||||||
if state == State.PAUSED:
|
if state == State.PAUSED:
|
||||||
if len(self.lastframe) == 0:
|
if self.lastframe is None:
|
||||||
_, self.lastframe = self.current_cap.read()
|
_, self.lastframe = self.current_cap.read()
|
||||||
return self.lastframe, state
|
return self.lastframe, state
|
||||||
still_playing, frame = self.current_cap.read()
|
still_playing, frame = self.current_cap.read()
|
||||||
@ -145,36 +211,31 @@ class Presentation:
|
|||||||
self.lastframe = frame
|
self.lastframe = frame
|
||||||
elif state in [state.WAIT, state.PAUSED]:
|
elif state in [state.WAIT, state.PAUSED]:
|
||||||
return self.lastframe, state
|
return self.lastframe, state
|
||||||
elif self.current_slide["type"] == "last" and self.current_slide["terminated"]:
|
elif self.current_slide.is_last() and self.current_slide.terminated:
|
||||||
return self.lastframe, State.END
|
return self.lastframe, State.END
|
||||||
|
else: # not still playing
|
||||||
if not still_playing:
|
if self.is_last_animation:
|
||||||
if self.current_slide["end_animation"] == self.current_animation + 1:
|
if self.current_slide.is_slide():
|
||||||
if self.current_slide["type"] == "slide":
|
|
||||||
# To fix "it always ends one frame before the animation", uncomment this.
|
|
||||||
# But then clears on the next slide will clear the stationary after this slide.
|
|
||||||
if self.last_frame_next:
|
|
||||||
self.load_this_cap(self.next_cap)
|
|
||||||
self.next_cap = self.caps[self.current_animation + 1]
|
|
||||||
|
|
||||||
self.next_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
||||||
_, self.lastframe = self.next_cap.read()
|
|
||||||
state = State.WAIT
|
state = State.WAIT
|
||||||
elif self.current_slide["type"] == "loop":
|
elif self.current_slide.is_loop():
|
||||||
self.current_animation = self.current_slide["start_animation"]
|
if self.reverse:
|
||||||
|
state = State.WAIT
|
||||||
|
else:
|
||||||
|
self.current_animation = self.current_slide.start_animation
|
||||||
state = State.PLAYING
|
state = State.PLAYING
|
||||||
self.rewind_slide()
|
self.rewind_current_slide()
|
||||||
elif self.current_slide["type"] == "last":
|
elif self.current_slide.is_last():
|
||||||
self.current_slide["terminated"] = True
|
self.current_slide.terminated = True
|
||||||
elif (
|
elif (
|
||||||
self.current_slide["type"] == "last"
|
self.current_slide.is_last()
|
||||||
and self.current_slide["end_animation"] == self.current_animation
|
and self.current_slide.end_animation == self.current_animation
|
||||||
):
|
):
|
||||||
|
print("HERE")
|
||||||
state = State.WAIT
|
state = State.WAIT
|
||||||
else:
|
else:
|
||||||
# Play next video!
|
# Play next video!
|
||||||
self.current_animation += 1
|
self.current_animation = self.next_animation
|
||||||
self.load_this_cap(self.current_animation)
|
self.load_animation_cap(self.current_animation)
|
||||||
# Reset video to position zero if it has been played before
|
# Reset video to position zero if it has been played before
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||||
|
|
||||||
@ -182,6 +243,8 @@ class Presentation:
|
|||||||
|
|
||||||
|
|
||||||
class Display:
|
class Display:
|
||||||
|
"""Displays one or more presentations one after each other."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
presentations,
|
presentations,
|
||||||
@ -199,7 +262,8 @@ class Display:
|
|||||||
|
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
self.lastframe = None
|
self.lastframe = None
|
||||||
self.current_presentation_i = 0
|
self.current_presentation_index = 0
|
||||||
|
self.exit = False
|
||||||
|
|
||||||
self.lag = 0
|
self.lag = 0
|
||||||
self.last_time = now()
|
self.last_time = now()
|
||||||
@ -217,6 +281,12 @@ class Display:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def resize_frame_to_screen(self, frame: np.ndarray):
|
def resize_frame_to_screen(self, frame: np.ndarray):
|
||||||
|
"""
|
||||||
|
Resizes a given frame to match screen dimensions.
|
||||||
|
|
||||||
|
Only works on Windows.
|
||||||
|
"""
|
||||||
|
assert self.is_windows, "Only Windows platforms need this method"
|
||||||
frame_height, frame_width = frame.shape[:2]
|
frame_height, frame_width = frame.shape[:2]
|
||||||
|
|
||||||
scale_height = self.screen_height / frame_height
|
scale_height = self.screen_height / frame_height
|
||||||
@ -227,11 +297,13 @@ class Display:
|
|||||||
return cv2.resize(frame, (int(scale * frame_height), int(scale * frame_width)))
|
return cv2.resize(frame, (int(scale * frame_height), int(scale * frame_width)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_presentation(self):
|
def current_presentation(self) -> Presentation:
|
||||||
return self.presentations[self.current_presentation_i]
|
"""Returns the current presentation"""
|
||||||
|
return self.presentations[self.current_presentation_index]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
"""Runs a series of presentations until end or exit."""
|
||||||
|
while not self.exit:
|
||||||
self.lastframe, self.state = self.current_presentation.update_state(
|
self.lastframe, self.state = self.current_presentation.update_state(
|
||||||
self.state
|
self.state
|
||||||
)
|
)
|
||||||
@ -240,16 +312,18 @@ class Display:
|
|||||||
self.state = State.PAUSED
|
self.state = State.PAUSED
|
||||||
self.start_paused = False
|
self.start_paused = False
|
||||||
if self.state == State.END:
|
if self.state == State.END:
|
||||||
if self.current_presentation_i == len(self.presentations) - 1:
|
if self.current_presentation_index == len(self.presentations) - 1:
|
||||||
self.quit()
|
self.quit()
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
self.current_presentation_i += 1
|
self.current_presentation_index += 1
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
self.handle_key()
|
self.handle_key()
|
||||||
self.show_video()
|
self.show_video()
|
||||||
self.show_info()
|
self.show_info()
|
||||||
|
|
||||||
def show_video(self):
|
def show_video(self):
|
||||||
|
"""Shows updated video."""
|
||||||
self.lag = now() - self.last_time
|
self.lag = now() - self.last_time
|
||||||
self.last_time = now()
|
self.last_time = now()
|
||||||
|
|
||||||
@ -261,6 +335,7 @@ class Display:
|
|||||||
cv2.imshow(WINDOW_NAME, frame)
|
cv2.imshow(WINDOW_NAME, frame)
|
||||||
|
|
||||||
def show_info(self):
|
def show_info(self):
|
||||||
|
"""Shows updated information about presentations."""
|
||||||
info = np.zeros((130, 420), np.uint8)
|
info = np.zeros((130, 420), np.uint8)
|
||||||
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
||||||
grid_x = [30, 230]
|
grid_x = [30, 230]
|
||||||
@ -276,20 +351,20 @@ class Display:
|
|||||||
|
|
||||||
cv2.putText(
|
cv2.putText(
|
||||||
info,
|
info,
|
||||||
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
|
f"Slide {self.current_presentation.current_slide.number}/{len(self.current_presentation.slides)}",
|
||||||
(grid_x[0], grid_y[1]),
|
(grid_x[0], grid_y[1]),
|
||||||
*font_args,
|
*font_args,
|
||||||
)
|
)
|
||||||
cv2.putText(
|
cv2.putText(
|
||||||
info,
|
info,
|
||||||
f"Slide Type: {self.current_presentation.current_slide['type']}",
|
f"Slide Type: {self.current_presentation.current_slide.type}",
|
||||||
(grid_x[1], grid_y[1]),
|
(grid_x[1], grid_y[1]),
|
||||||
*font_args,
|
*font_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
cv2.putText(
|
cv2.putText(
|
||||||
info,
|
info,
|
||||||
f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}",
|
f"Scene {self.current_presentation_index + 1}/{len(self.presentations)}",
|
||||||
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
|
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
|
||||||
*font_args,
|
*font_args,
|
||||||
)
|
)
|
||||||
@ -297,6 +372,7 @@ class Display:
|
|||||||
cv2.imshow(f"{WINDOW_NAME}: Info", info)
|
cv2.imshow(f"{WINDOW_NAME}: Info", info)
|
||||||
|
|
||||||
def handle_key(self):
|
def handle_key(self):
|
||||||
|
"""Handles key strokes."""
|
||||||
sleep_time = math.ceil(1000 / self.current_presentation.fps)
|
sleep_time = math.ceil(1000 / self.current_presentation.fps)
|
||||||
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
|
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
|
||||||
|
|
||||||
@ -309,30 +385,34 @@ class Display:
|
|||||||
elif self.state == State.WAIT and (
|
elif self.state == State.WAIT and (
|
||||||
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
|
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
|
||||||
):
|
):
|
||||||
self.current_presentation.next()
|
self.current_presentation.load_next_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
elif (
|
elif (
|
||||||
self.state == State.PLAYING and self.config.CONTINUE.match(key)
|
self.state == State.PLAYING and self.config.CONTINUE.match(key)
|
||||||
) or self.skip_all:
|
) or self.skip_all:
|
||||||
self.current_presentation.next()
|
self.current_presentation.load_next_slide()
|
||||||
elif self.config.BACK.match(key):
|
elif self.config.BACK.match(key):
|
||||||
if self.current_presentation.current_slide_i == 0:
|
if self.current_presentation.current_slide_index == 0:
|
||||||
self.current_presentation_i = max(0, self.current_presentation_i - 1)
|
if self.current_presentation_index == 0:
|
||||||
self.current_presentation.start_last_slide()
|
self.current_presentation.rewind_current_slide()
|
||||||
|
else:
|
||||||
|
self.current_presentation_index -= 1
|
||||||
|
self.current_presentation.load_last_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
else:
|
else:
|
||||||
self.current_presentation.prev()
|
self.current_presentation.load_previous_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
elif self.config.REVERSE.match(key):
|
elif self.config.REVERSE.match(key):
|
||||||
self.current_presentation.reverse_slide()
|
self.current_presentation.reverse_current_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
elif self.config.REWIND.match(key):
|
elif self.config.REWIND.match(key):
|
||||||
self.current_presentation.rewind_slide()
|
self.current_presentation.rewind_current_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
|
"""Destroys all windows created by presentations and exits gracefully."""
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
sys.exit()
|
self.exit = True
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@ -350,12 +430,18 @@ def list_scenes(folder):
|
|||||||
click.secho(f"{i}: {scene}", fg="green")
|
click.secho(f"{i}: {scene}", fg="green")
|
||||||
|
|
||||||
|
|
||||||
def _list_scenes(folder):
|
def _list_scenes(folder) -> List[str]:
|
||||||
|
"""Lists available scenes in given directory."""
|
||||||
scenes = []
|
scenes = []
|
||||||
|
|
||||||
for file in os.listdir(folder):
|
for file in os.listdir(folder):
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
|
try:
|
||||||
|
filepath = os.path.join(folder, file)
|
||||||
|
_ = PresentationConfig.parse_file(filepath)
|
||||||
scenes.append(os.path.basename(file)[:-5])
|
scenes.append(os.path.basename(file)[:-5])
|
||||||
|
except Exception as e: # Could not parse this file as a proper presentation config
|
||||||
|
pass
|
||||||
|
|
||||||
return scenes
|
return scenes
|
||||||
|
|
||||||
@ -371,20 +457,13 @@ def _list_scenes(folder):
|
|||||||
)
|
)
|
||||||
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||||
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
|
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
|
||||||
@click.option(
|
|
||||||
"--last-frame-next",
|
|
||||||
is_flag=True,
|
|
||||||
help="Show the next animation first frame as last frame (hack).",
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--skip-all",
|
"--skip-all",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Skip all slides, useful the test if slides are working.",
|
help="Skip all slides, useful the test if slides are working.",
|
||||||
)
|
)
|
||||||
@click.help_option("-h", "--help")
|
@click.help_option("-h", "--help")
|
||||||
def present(
|
def present(scenes, config_path, folder, start_paused, fullscreen, skip_all):
|
||||||
scenes, config_path, folder, start_paused, fullscreen, last_frame_next, skip_all
|
|
||||||
):
|
|
||||||
"""Present the different scenes."""
|
"""Present the different scenes."""
|
||||||
|
|
||||||
if len(scenes) == 0:
|
if len(scenes) == 0:
|
||||||
@ -429,11 +508,17 @@ def present(
|
|||||||
raise click.UsageError(
|
raise click.UsageError(
|
||||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||||
)
|
)
|
||||||
config = json.load(open(config_file))
|
try:
|
||||||
presentations.append(Presentation(config, last_frame_next=last_frame_next))
|
config = PresentationConfig.parse_file(config_file)
|
||||||
|
presentations.append(Presentation(config))
|
||||||
|
except ValidationError as e:
|
||||||
|
raise click.UsageError(str(e))
|
||||||
|
|
||||||
if os.path.exists(config_path):
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
config = Config.parse_file(config_path)
|
config = Config.parse_file(config_path)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise click.UsageError(str(e))
|
||||||
else:
|
else:
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
|
@ -3,25 +3,27 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from .config import PresentationConfig, SlideConfig, SlideType
|
||||||
from .defaults import FOLDER_PATH
|
from .defaults import FOLDER_PATH
|
||||||
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
||||||
|
|
||||||
|
|
||||||
def reverse_video_path(src: str) -> str:
|
|
||||||
file, ext = os.path.splitext(src)
|
|
||||||
return f"{file}_reversed{ext}"
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_video_file(src: str, dst: str):
|
def reverse_video_file(src: str, dst: str):
|
||||||
|
"""Reverses a video file, writting the result to `dst`."""
|
||||||
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
||||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
|
|
||||||
|
|
||||||
class Slide(Scene):
|
class Slide(Scene):
|
||||||
|
"""
|
||||||
|
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
|
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
if not os.path.isdir("videos"):
|
if not os.path.isdir("videos"):
|
||||||
@ -44,7 +46,8 @@ class Slide(Scene):
|
|||||||
self.pause_start_animation = 0
|
self.pause_start_animation = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def partial_movie_files(self):
|
def partial_movie_files(self) -> List[str]:
|
||||||
|
"""Returns a list of partial movie files, a.k.a animations."""
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||||
|
|
||||||
@ -59,7 +62,8 @@ class Slide(Scene):
|
|||||||
return self.renderer.file_writer.partial_movie_files
|
return self.renderer.file_writer.partial_movie_files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def show_progress_bar(self):
|
def show_progress_bar(self) -> bool:
|
||||||
|
"""Returns True if progress bar should be displayed."""
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
return getattr(super(Scene, self), "show_progress_bar", True)
|
return getattr(super(Scene, self), "show_progress_bar", True)
|
||||||
else:
|
else:
|
||||||
@ -67,19 +71,22 @@ class Slide(Scene):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def leave_progress_bar(self):
|
def leave_progress_bar(self):
|
||||||
|
"""Returns True if progress bar should be left after completed."""
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
return getattr(super(Scene, self), "leave_progress_bars", False)
|
return getattr(super(Scene, self), "leave_progress_bars", False)
|
||||||
else:
|
else:
|
||||||
return config["progress_bar"] == "leave"
|
return config["progress_bar"] == "leave"
|
||||||
|
|
||||||
def play(self, *args, **kwargs):
|
def play(self, *args, **kwargs):
|
||||||
|
"""Overloads `self.play` and increment animation count."""
|
||||||
super().play(*args, **kwargs)
|
super().play(*args, **kwargs)
|
||||||
self.current_animation += 1
|
self.current_animation += 1
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
|
"""Creates a new slide with previous animations."""
|
||||||
self.slides.append(
|
self.slides.append(
|
||||||
dict(
|
SlideConfig(
|
||||||
type="slide",
|
type=SlideType.slide,
|
||||||
start_animation=self.pause_start_animation,
|
start_animation=self.pause_start_animation,
|
||||||
end_animation=self.current_animation,
|
end_animation=self.current_animation,
|
||||||
number=self.current_slide,
|
number=self.current_slide,
|
||||||
@ -89,16 +96,18 @@ class Slide(Scene):
|
|||||||
self.pause_start_animation = self.current_animation
|
self.pause_start_animation = self.current_animation
|
||||||
|
|
||||||
def start_loop(self):
|
def start_loop(self):
|
||||||
|
"""Starts a loop."""
|
||||||
assert self.loop_start_animation is None, "You cannot nest loops"
|
assert self.loop_start_animation is None, "You cannot nest loops"
|
||||||
self.loop_start_animation = self.current_animation
|
self.loop_start_animation = self.current_animation
|
||||||
|
|
||||||
def end_loop(self):
|
def end_loop(self):
|
||||||
|
"""Ends an existing loop."""
|
||||||
assert (
|
assert (
|
||||||
self.loop_start_animation is not None
|
self.loop_start_animation is not None
|
||||||
), "You have to start a loop before ending it"
|
), "You have to start a loop before ending it"
|
||||||
self.slides.append(
|
self.slides.append(
|
||||||
dict(
|
SlideConfig(
|
||||||
type="loop",
|
type=SlideType.loop,
|
||||||
start_animation=self.loop_start_animation,
|
start_animation=self.loop_start_animation,
|
||||||
end_animation=self.current_animation,
|
end_animation=self.current_animation,
|
||||||
number=self.current_slide,
|
number=self.current_slide,
|
||||||
@ -109,7 +118,11 @@ class Slide(Scene):
|
|||||||
self.pause_start_animation = self.current_animation
|
self.pause_start_animation = self.current_animation
|
||||||
|
|
||||||
def save_slides(self, use_cache=True):
|
def save_slides(self, use_cache=True):
|
||||||
|
"""
|
||||||
|
Saves slides, optionally using cached files.
|
||||||
|
|
||||||
|
Note that cached files only work with Manim.
|
||||||
|
"""
|
||||||
if not os.path.exists(self.output_folder):
|
if not os.path.exists(self.output_folder):
|
||||||
os.mkdir(self.output_folder)
|
os.mkdir(self.output_folder)
|
||||||
|
|
||||||
@ -139,9 +152,7 @@ class Slide(Scene):
|
|||||||
disable=not self.show_progress_bar,
|
disable=not self.show_progress_bar,
|
||||||
):
|
):
|
||||||
filename = os.path.basename(src_file)
|
filename = os.path.basename(src_file)
|
||||||
_hash, ext = os.path.splitext(filename)
|
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
|
||||||
|
|
||||||
rev_filename = f"{_hash}_reversed{ext}"
|
|
||||||
|
|
||||||
dst_file = os.path.join(scene_files_folder, filename)
|
dst_file = os.path.join(scene_files_folder, filename)
|
||||||
# We only copy animation if it was not present
|
# We only copy animation if it was not present
|
||||||
@ -165,9 +176,9 @@ class Slide(Scene):
|
|||||||
|
|
||||||
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
|
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
|
||||||
|
|
||||||
f = open(slide_path, "w")
|
with open(slide_path, "w") as f:
|
||||||
json.dump(dict(slides=self.slides, files=files), f)
|
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
|
||||||
f.close()
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
||||||
)
|
)
|
||||||
@ -191,4 +202,10 @@ class Slide(Scene):
|
|||||||
|
|
||||||
|
|
||||||
class ThreeDSlide(Slide, ThreeDScene):
|
class ThreeDSlide(Slide, ThreeDScene):
|
||||||
|
"""
|
||||||
|
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
|
||||||
|
|
||||||
|
Note that ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
13
setup.py
13
setup.py
@ -1,18 +1,25 @@
|
|||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
from manim_slides import __version__ as version
|
|
||||||
|
|
||||||
if sys.version_info < (3, 7):
|
if sys.version_info < (3, 7):
|
||||||
raise RuntimeError("This package requires Python 3.7+")
|
raise RuntimeError("This package requires Python 3.7+")
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"__version__", os.path.join("manim_slides", "__version__.py")
|
||||||
|
)
|
||||||
|
version = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(version)
|
||||||
|
|
||||||
|
|
||||||
with open("README.md", "r") as f:
|
with open("README.md", "r") as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="manim-slides",
|
name="manim-slides",
|
||||||
version=version,
|
version=version.__version__,
|
||||||
author="Jérome Eertmans (previously, Federico A. Galatolo)",
|
author="Jérome Eertmans (previously, Federico A. Galatolo)",
|
||||||
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
|
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
|
||||||
description="Tool for live presentations using manim",
|
description="Tool for live presentations using manim",
|
||||||
|
Reference in New Issue
Block a user