mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-21 20:46:01 +08:00
Refactor old work
This commit is contained in:
@ -1 +0,0 @@
|
||||
from manim_presentation.slide import Slide
|
@ -1,51 +0,0 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
def prompt(question):
|
||||
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
||||
display = np.zeros((130, 420), np.uint8)
|
||||
|
||||
cv2.putText(
|
||||
display,
|
||||
"* Manim Presentation Wizard *",
|
||||
(50, 33),
|
||||
*font_args
|
||||
)
|
||||
cv2.putText(
|
||||
display,
|
||||
question,
|
||||
(30, 85),
|
||||
*font_args
|
||||
)
|
||||
|
||||
cv2.imshow("wizard", display)
|
||||
return cv2.waitKeyEx(-1)
|
||||
|
||||
def main():
|
||||
if(os.path.exists("./manim-presentation.json")):
|
||||
print("The manim-presentation.json configuration file exists")
|
||||
ans = input("Do you want to continue and overwrite it? (y/n): ")
|
||||
if ans != "y": sys.exit(0)
|
||||
|
||||
prompt("Press any key to continue")
|
||||
PLAYPAUSE_KEY = prompt("Press the PLAY/PAUSE key")
|
||||
CONTINUE_KEY = prompt("Press the CONTINUE/NEXT key")
|
||||
BACK_KEY = prompt("Press the BACK key")
|
||||
REWIND_KEY = prompt("Press the REWIND key")
|
||||
QUIT_KEY = prompt("Press the QUIT key")
|
||||
|
||||
config_file = open("./manim-presentation.json", "w")
|
||||
json.dump(dict(
|
||||
PLAYPAUSE_KEY=PLAYPAUSE_KEY,
|
||||
CONTINUE_KEY=CONTINUE_KEY,
|
||||
BACK_KEY=BACK_KEY,
|
||||
REWIND_KEY=REWIND_KEY,
|
||||
QUIT_KEY=QUIT_KEY
|
||||
), config_file)
|
||||
config_file.close()
|
||||
|
||||
|
||||
|
3
manim_slides/__init__.py
Normal file
3
manim_slides/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .slide import Slide, ThreeDSlide
|
||||
|
||||
__version__ = "3.0.0"
|
27
manim_slides/commons.py
Normal file
27
manim_slides/commons.py
Normal file
@ -0,0 +1,27 @@
|
||||
import click
|
||||
from .defaults import CONFIG_PATH
|
||||
|
||||
|
||||
def config_path_option(function):
|
||||
return click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_path",
|
||||
default=CONFIG_PATH,
|
||||
type=click.Path(dir_okay=False),
|
||||
help="Set path to configuration file.",
|
||||
)(function)
|
||||
|
||||
|
||||
def config_options(function):
|
||||
function = config_path_option(function)
|
||||
function = click.option(
|
||||
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."
|
||||
)(function)
|
||||
function = click.option(
|
||||
"-m",
|
||||
"--merge",
|
||||
is_flag=True,
|
||||
help="Merge any existing configuration file with the new configuration.",
|
||||
)(function)
|
||||
return function
|
48
manim_slides/config.py
Normal file
48
manim_slides/config.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Optional, Set
|
||||
|
||||
from pydantic import BaseModel, root_validator, validator
|
||||
|
||||
from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
|
||||
|
||||
|
||||
class Key(BaseModel):
|
||||
ids: Set[int]
|
||||
name: Optional[str] = None
|
||||
|
||||
@validator("ids", each_item=True)
|
||||
def id_is_posint(cls, v: int):
|
||||
if v < 0:
|
||||
raise ValueError("Key ids cannot be negative integers")
|
||||
return v
|
||||
|
||||
def match(self, key_id: int):
|
||||
return key_id in self.ids
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
QUIT: Key = Key(ids=[ord("q")], name="QUIT")
|
||||
CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT")
|
||||
BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK")
|
||||
REWIND: Key = Key(ids=[ord("r")], name="REWIND")
|
||||
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
|
||||
|
||||
@root_validator
|
||||
def ids_are_unique_across_keys(cls, values):
|
||||
ids = set()
|
||||
|
||||
for key in values.values():
|
||||
if len(ids.intersection(key.ids)) != 0:
|
||||
raise ValueError(
|
||||
f"Two or more keys share a common key code: please make sure each key has distinc key codes"
|
||||
)
|
||||
ids.update(key.ids)
|
||||
|
||||
return values
|
||||
|
||||
def merge_with(self, other: "Config") -> "Config":
|
||||
for key_name, key in self:
|
||||
other_key = getattr(other, key_name)
|
||||
key.ids.update(other_key.ids)
|
||||
key.name = other_key.name or key.name
|
||||
|
||||
return self
|
11
manim_slides/defaults.py
Normal file
11
manim_slides/defaults.py
Normal file
@ -0,0 +1,11 @@
|
||||
import platform
|
||||
|
||||
FOLDER_PATH: str = "./slides"
|
||||
CONFIG_PATH: str = ".manim-slides.json"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
RIGHT_ARROW_KEY_CODE = 2555904
|
||||
LEFT_ARROW_KEY_CODE = 2424832
|
||||
else:
|
||||
RIGHT_ARROW_KEY_CODE = 65363
|
||||
LEFT_ARROW_KEY_CODE = 65361
|
20
manim_slides/main.py
Normal file
20
manim_slides/main.py
Normal file
@ -0,0 +1,20 @@
|
||||
import click
|
||||
from click_default_group import DefaultGroup
|
||||
|
||||
from . import __version__
|
||||
from .present import present
|
||||
from .wizard import wizard, init
|
||||
|
||||
|
||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||
@click.version_option(__version__, "-v", "--version")
|
||||
@click.help_option("-h", "--help")
|
||||
def cli():
|
||||
pass
|
||||
|
||||
cli.add_command(present)
|
||||
cli.add_command(wizard)
|
||||
cli.add_command(init)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1,34 +1,18 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
from enum import Enum
|
||||
import platform
|
||||
|
||||
class Config:
|
||||
@classmethod
|
||||
def init(cls):
|
||||
if platform.system() == "Windows":
|
||||
cls.QUIT_KEY = ord("q")
|
||||
cls.CONTINUE_KEY = 2555904 #right arrow
|
||||
cls.BACK_KEY = 2424832 #left arrow
|
||||
cls.REWIND_KEY = ord("r")
|
||||
cls.PLAYPAUSE_KEY = 32 #spacebar
|
||||
else:
|
||||
cls.QUIT_KEY = ord("q")
|
||||
cls.CONTINUE_KEY = 65363 #right arrow
|
||||
cls.BACK_KEY = 65361 #left arrow
|
||||
cls.REWIND_KEY = ord("r")
|
||||
cls.PLAYPAUSE_KEY = 32 #spacebar
|
||||
import click
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .config import Config
|
||||
from .defaults import CONFIG_PATH, FOLDER_PATH
|
||||
from .commons import config_path_option
|
||||
|
||||
if os.path.exists(os.path.join(os.getcwd(), "./manim-presentation.json")):
|
||||
json_config = json.load(open(os.path.join(os.getcwd(), "./manim-presentation.json"), "r"))
|
||||
for key, value in json_config.items():
|
||||
setattr(cls, key, value)
|
||||
|
||||
class State(Enum):
|
||||
PLAYING = 0
|
||||
@ -50,7 +34,7 @@ def fix_time(x):
|
||||
return x if x > 0 else 1
|
||||
|
||||
class Presentation:
|
||||
def __init__(self, config, last_frame_next=False):
|
||||
def __init__(self, config, last_frame_next: bool = False):
|
||||
self.last_frame_next = last_frame_next
|
||||
self.slides = config["slides"]
|
||||
self.files = config["files"]
|
||||
@ -164,9 +148,10 @@ class Presentation:
|
||||
|
||||
|
||||
class Display:
|
||||
def __init__(self, presentations, start_paused=False, fullscreen=False):
|
||||
def __init__(self, presentations, config, start_paused=False, fullscreen=False):
|
||||
self.presentations = presentations
|
||||
self.start_paused = start_paused
|
||||
self.config = config
|
||||
|
||||
self.state = State.PLAYING
|
||||
self.lastframe = None
|
||||
@ -250,18 +235,18 @@ class Display:
|
||||
sleep_time = math.ceil(1000/self.current_presentation.fps)
|
||||
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
|
||||
|
||||
if key == Config.QUIT_KEY:
|
||||
if self.config.QUIT.match(key):
|
||||
self.quit()
|
||||
elif self.state == State.PLAYING and key == Config.PLAYPAUSE_KEY:
|
||||
elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key):
|
||||
self.state = State.PAUSED
|
||||
elif self.state == State.PAUSED and key == Config.PLAYPAUSE_KEY:
|
||||
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
|
||||
self.state = State.PLAYING
|
||||
elif self.state == State.WAIT and (key == Config.CONTINUE_KEY or key == Config.PLAYPAUSE_KEY):
|
||||
elif self.state == State.WAIT and (self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)):
|
||||
self.current_presentation.next()
|
||||
self.state = State.PLAYING
|
||||
elif self.state == State.PLAYING and key == Config.CONTINUE_KEY:
|
||||
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
|
||||
self.current_presentation.next()
|
||||
elif key == Config.BACK_KEY:
|
||||
elif self.config.BACK.match(key):
|
||||
if self.current_presentation.current_slide_i == 0:
|
||||
self.current_presentation_i = max(0, self.current_presentation_i - 1)
|
||||
self.current_presentation.reset()
|
||||
@ -269,7 +254,7 @@ class Display:
|
||||
else:
|
||||
self.current_presentation.prev()
|
||||
self.state = State.PLAYING
|
||||
elif key == Config.REWIND_KEY:
|
||||
elif self.config.REWIND.match(key):
|
||||
self.current_presentation.rewind_slide()
|
||||
self.state = State.PLAYING
|
||||
|
||||
@ -279,30 +264,29 @@ class Display:
|
||||
sys.exit()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("scenes", metavar="scenes", type=str, nargs="+", help="Scenes to present")
|
||||
parser.add_argument("--folder", type=str, default="./presentation", help="Presentation files folder")
|
||||
parser.add_argument("--start-paused", action="store_true", help="Start paused")
|
||||
parser.add_argument("--fullscreen", action="store_true", help="Fullscreen")
|
||||
parser.add_argument("--last-frame-next", action="store_true", help="Show the next animation first frame as last frame (hack)")
|
||||
|
||||
args = parser.parse_args()
|
||||
args.folder = os.path.normcase(args.folder)
|
||||
|
||||
Config.init()
|
||||
@click.command()
|
||||
@click.argument("scenes", nargs=-1)
|
||||
@config_path_option
|
||||
@click.option("--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.")
|
||||
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||
@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.help_option("-h", "--help")
|
||||
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
|
||||
"""Present the different scenes"""
|
||||
|
||||
presentations = list()
|
||||
for scene in args.scenes:
|
||||
config_file = os.path.join(args.folder, f"{scene}.json")
|
||||
for scene in scenes:
|
||||
config_file = os.path.join(folder, f"{scene}.json")
|
||||
if not os.path.exists(config_file):
|
||||
raise Exception(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))
|
||||
presentations.append(Presentation(config, last_frame_next=args.last_frame_next))
|
||||
presentations.append(Presentation(config, last_frame_next=last_frame_next))
|
||||
|
||||
display = Display(presentations, start_paused=args.start_paused, fullscreen=args.fullscreen)
|
||||
if os.path.exists(config_path):
|
||||
config = Config.parse_file(config_path)
|
||||
else:
|
||||
config = Config()
|
||||
|
||||
display = Display(presentations, config=config, start_paused=start_paused, fullscreen=fullscreen)
|
||||
display.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,13 +1,16 @@
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from manim import Scene, config
|
||||
from manim.animation.animation import Wait
|
||||
|
||||
from manim import Scene, ThreeDScene, config
|
||||
|
||||
from .defaults import FOLDER_PATH
|
||||
|
||||
|
||||
class Slide(Scene):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.output_folder = kwargs.pop("output_folder", "./presentation")
|
||||
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
|
||||
super(Slide, self).__init__(*args, **kwargs)
|
||||
self.output_folder = output_folder
|
||||
self.slides = list()
|
||||
self.current_slide = 1
|
||||
self.current_animation = 0
|
||||
@ -29,7 +32,7 @@ class Slide(Scene):
|
||||
self.pause_start_animation = self.current_animation
|
||||
|
||||
def start_loop(self):
|
||||
assert self.loop_start_animation is None, "You cant nest loops"
|
||||
assert self.loop_start_animation is None, "You cannot nest loops"
|
||||
self.loop_start_animation = self.current_animation
|
||||
|
||||
def end_loop(self):
|
||||
@ -81,3 +84,6 @@ class Slide(Scene):
|
||||
files=files
|
||||
), f)
|
||||
f.close()
|
||||
|
||||
class ThreeDSlide(ThreeDScene, Slide):
|
||||
pass
|
71
manim_slides/wizard.py
Normal file
71
manim_slides/wizard.py
Normal file
@ -0,0 +1,71 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import click
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .config import Config
|
||||
from .commons import config_options
|
||||
from .defaults import CONFIG_PATH
|
||||
|
||||
|
||||
def prompt(question: str) -> int:
|
||||
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
||||
display = np.zeros((130, 420), np.uint8)
|
||||
|
||||
cv2.putText(display, "* Manim Slides Wizard *", (70, 33), *font_args)
|
||||
cv2.putText(display, question, (30, 85), *font_args)
|
||||
|
||||
cv2.imshow("Manim Slides Configuration Wizard", display)
|
||||
return cv2.waitKeyEx(-1)
|
||||
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
def wizard(config_path, force, merge):
|
||||
"""Launch configuration wizard."""
|
||||
return _init(config_path, force, merge, skip_interactive=False)
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
def init(config_path, force, merge, skip_interactive=False):
|
||||
"""Initialize a new default configuration file."""
|
||||
return _init(config_path, force, merge, skip_interactive=True)
|
||||
|
||||
def _init(config_path, force, merge, skip_interactive=False):
|
||||
|
||||
if os.path.exists(config_path):
|
||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||
|
||||
if not force and not merge:
|
||||
choice = click.prompt("Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?", type=click.Choice(["o", "m", "q"], case_sensitive=False))
|
||||
|
||||
force = choice == "o"
|
||||
merge = choice == "m"
|
||||
|
||||
if force:
|
||||
click.secho("Overwriting.")
|
||||
elif merge:
|
||||
click.secho("Merging.")
|
||||
else:
|
||||
click.secho("Exiting.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
if not skip_interactive:
|
||||
|
||||
prompt("Press any key to continue")
|
||||
|
||||
for _, key in config:
|
||||
key.ids = [prompt(f"Press the {key.name} key")]
|
||||
|
||||
if merge:
|
||||
config = Config.parse_file(config_path).merge_with(config)
|
||||
|
||||
with open(config_path, "w") as config_file:
|
||||
config_file.write(config.json(indent=4))
|
||||
|
||||
click.echo(f"Configuration file successfully save to `{config_path}`")
|
Reference in New Issue
Block a user