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 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()
|
|
@ -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
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