Refactor old work

This commit is contained in:
Jérome Eertmans
2022-07-12 17:11:32 +02:00
parent ddeb20646d
commit f3d2c4e731
10 changed files with 232 additions and 114 deletions

View File

@ -1 +0,0 @@
from manim_presentation.slide import Slide

View File

@ -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
View File

@ -0,0 +1,3 @@
from .slide import Slide, ThreeDSlide
__version__ = "3.0.0"

27
manim_slides/commons.py Normal file
View 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
View 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
View 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
View 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()

View File

@ -1,34 +1,18 @@
import cv2
import numpy as np
import os
import sys
import json import json
import math import math
import os
import sys
import time import time
import argparse
from enum import Enum from enum import Enum
import platform
class Config: import click
@classmethod import cv2
def init(cls): import numpy as np
if platform.system() == "Windows":
cls.QUIT_KEY = ord("q") from .config import Config
cls.CONTINUE_KEY = 2555904 #right arrow from .defaults import CONFIG_PATH, FOLDER_PATH
cls.BACK_KEY = 2424832 #left arrow from .commons import config_path_option
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
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): class State(Enum):
PLAYING = 0 PLAYING = 0
@ -50,7 +34,7 @@ def fix_time(x):
return x if x > 0 else 1 return x if x > 0 else 1
class Presentation: 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.last_frame_next = last_frame_next
self.slides = config["slides"] self.slides = config["slides"]
self.files = config["files"] self.files = config["files"]
@ -164,9 +148,10 @@ class Presentation:
class Display: 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.presentations = presentations
self.start_paused = start_paused self.start_paused = start_paused
self.config = config
self.state = State.PLAYING self.state = State.PLAYING
self.lastframe = None self.lastframe = None
@ -250,18 +235,18 @@ class Display:
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))
if key == Config.QUIT_KEY: if self.config.QUIT.match(key):
self.quit() 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 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 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.current_presentation.next()
self.state = State.PLAYING 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() self.current_presentation.next()
elif key == Config.BACK_KEY: elif self.config.BACK.match(key):
if self.current_presentation.current_slide_i == 0: if self.current_presentation.current_slide_i == 0:
self.current_presentation_i = max(0, self.current_presentation_i - 1) self.current_presentation_i = max(0, self.current_presentation_i - 1)
self.current_presentation.reset() self.current_presentation.reset()
@ -269,7 +254,7 @@ class Display:
else: else:
self.current_presentation.prev() self.current_presentation.prev()
self.state = State.PLAYING self.state = State.PLAYING
elif key == Config.REWIND_KEY: elif self.config.REWIND.match(key):
self.current_presentation.rewind_slide() self.current_presentation.rewind_slide()
self.state = State.PLAYING self.state = State.PLAYING
@ -279,30 +264,29 @@ class Display:
sys.exit() sys.exit()
def main(): @click.command()
parser = argparse.ArgumentParser() @click.argument("scenes", nargs=-1)
@config_path_option
parser.add_argument("scenes", metavar="scenes", type=str, nargs="+", help="Scenes to present") @click.option("--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.")
parser.add_argument("--folder", type=str, default="./presentation", help="Presentation files folder") @click.option("--start-paused", is_flag=True, help="Start paused.")
parser.add_argument("--start-paused", action="store_true", help="Start paused") @click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
parser.add_argument("--fullscreen", action="store_true", help="Fullscreen") @click.option("--last-frame-next", is_flag=True, help="Show the next animation first frame as last frame (hack).")
parser.add_argument("--last-frame-next", action="store_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):
args = parser.parse_args() """Present the different scenes"""
args.folder = os.path.normcase(args.folder)
Config.init()
presentations = list() presentations = list()
for scene in args.scenes: for scene in scenes:
config_file = os.path.join(args.folder, f"{scene}.json") config_file = os.path.join(folder, f"{scene}.json")
if not os.path.exists(config_file): 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") 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)) 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() display.run()
if __name__ == "__main__":
main()

View File

@ -1,13 +1,16 @@
import os
import json import json
import os
import shutil 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): class Slide(Scene):
def __init__(self, *args, **kwargs): def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
self.output_folder = kwargs.pop("output_folder", "./presentation")
super(Slide, self).__init__(*args, **kwargs) super(Slide, self).__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides = list() self.slides = list()
self.current_slide = 1 self.current_slide = 1
self.current_animation = 0 self.current_animation = 0
@ -29,7 +32,7 @@ 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):
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 self.loop_start_animation = self.current_animation
def end_loop(self): def end_loop(self):
@ -81,3 +84,6 @@ class Slide(Scene):
files=files files=files
), f) ), f)
f.close() f.close()
class ThreeDSlide(ThreeDScene, Slide):
pass

71
manim_slides/wizard.py Normal file
View 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}`")