mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-21 20:46:01 +08:00
feat(cli): reverse videos on the fly
This commit is contained in:
@ -65,6 +65,7 @@ Default keybindings to control the presentation:
|
|||||||
| Right Arrow | Continue/Next Slide |
|
| Right Arrow | Continue/Next Slide |
|
||||||
| Left Arrow | Previous Slide |
|
| Left Arrow | Previous Slide |
|
||||||
| R | Re-Animate Current Slide |
|
| R | Re-Animate Current Slide |
|
||||||
|
| V | Reverse Current Slide |
|
||||||
| Spacebar | Play/Pause |
|
| Spacebar | Play/Pause |
|
||||||
| Q | Quit |
|
| Q | Quit |
|
||||||
|
|
||||||
@ -122,10 +123,10 @@ Here are a few things that I implemented (or that I'm planning to implement) on
|
|||||||
- [x] Only one cli (to rule them all)
|
- [x] Only one cli (to rule them all)
|
||||||
- [x] User can easily generate dummy config file
|
- [x] User can easily generate dummy config file
|
||||||
- [x] Config file path can be manually set
|
- [x] Config file path can be manually set
|
||||||
- [ ] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
|
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
|
||||||
- [x] Handle 3D scenes out of the box
|
- [x] Handle 3D scenes out of the box
|
||||||
- [ ] Generate docs online
|
- [ ] Generate docs online
|
||||||
- [ ] Fix the quality problem on Windows platforms with `fullscreen` flag
|
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
|
||||||
|
|
||||||
## Contributions and license
|
## Contributions and license
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "3.0.1"
|
__version__ = "3.1.0"
|
||||||
|
@ -23,6 +23,7 @@ class Config(BaseModel):
|
|||||||
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")
|
||||||
|
REVERSE: Key = Key(ids=[ord("v")], name="REVERSE")
|
||||||
REWIND: Key = Key(ids=[ord("r")], name="REWIND")
|
REWIND: Key = Key(ids=[ord("r")], name="REWIND")
|
||||||
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
|
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ class Config(BaseModel):
|
|||||||
for key in values.values():
|
for key in values.values():
|
||||||
if len(ids.intersection(key.ids)) != 0:
|
if len(ids.intersection(key.ids)) != 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Two or more keys share a common key code: please make sure each key has distinc key codes"
|
"Two or more keys share a common key code: please make sure each key has distinc key codes"
|
||||||
)
|
)
|
||||||
ids.update(key.ids)
|
ids.update(key.ids)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import click
|
|||||||
from click_default_group import DefaultGroup
|
from click_default_group import DefaultGroup
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .present import present
|
from .present import list_scenes, present
|
||||||
from .wizard import init, wizard
|
from .wizard import init, wizard
|
||||||
|
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ def cli():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
cli.add_command(list_scenes)
|
||||||
cli.add_command(present)
|
cli.add_command(present)
|
||||||
cli.add_command(wizard)
|
cli.add_command(wizard)
|
||||||
cli.add_command(init)
|
cli.add_command(init)
|
||||||
|
@ -42,7 +42,9 @@ class Presentation:
|
|||||||
def __init__(self, config, last_frame_next: bool = 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 = [reverse_video_path(path) for path in config["files"]]
|
self.files = [path for path in config["files"]]
|
||||||
|
self.reverse = False
|
||||||
|
self.reversed_slide = -1
|
||||||
|
|
||||||
self.lastframe = []
|
self.lastframe = []
|
||||||
|
|
||||||
@ -80,22 +82,34 @@ class Presentation:
|
|||||||
self.current_slide_i = max(0, self.current_slide_i - 1)
|
self.current_slide_i = max(0, self.current_slide_i - 1)
|
||||||
self.rewind_slide()
|
self.rewind_slide()
|
||||||
|
|
||||||
def reserve_slide(self):
|
def reverse_slide(self):
|
||||||
pass
|
self.rewind_slide(reverse=True)
|
||||||
|
|
||||||
def rewind_slide(self):
|
def rewind_slide(self, reverse: bool = False):
|
||||||
|
self.reverse = reverse
|
||||||
self.current_animation = self.current_slide["start_animation"]
|
self.current_animation = self.current_slide["start_animation"]
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||||
|
|
||||||
def load_this_cap(self, cap_number):
|
def load_this_cap(self, cap_number: int):
|
||||||
if self.caps[cap_number] == None:
|
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
|
# unload other caps
|
||||||
for i in range(len(self.caps)):
|
for i in range(len(self.caps)):
|
||||||
if self.caps[i] != None:
|
if not self.caps[i] is None:
|
||||||
self.caps[i].release()
|
self.caps[i].release()
|
||||||
self.caps[i] = None
|
self.caps[i] = None
|
||||||
# load this cap
|
# load this cap
|
||||||
self.caps[cap_number] = cv2.VideoCapture(self.files[cap_number])
|
file = self.files[cap_number]
|
||||||
|
if self.reverse:
|
||||||
|
self.reversed_slide = cap_number
|
||||||
|
file = "{}_reversed{}".format(*os.path.splitext(file))
|
||||||
|
else:
|
||||||
|
self.reversed_slide = -1
|
||||||
|
|
||||||
|
self.caps[cap_number] = cv2.VideoCapture(file)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_slide(self):
|
def current_slide(self):
|
||||||
@ -174,7 +188,9 @@ class Display:
|
|||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
user32 = ctypes.windll.user32
|
user32 = ctypes.windll.user32
|
||||||
self.screen_width, self.screen_height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)
|
self.screen_width, self.screen_height = user32.GetSystemMetrics(
|
||||||
|
0
|
||||||
|
), user32.GetSystemMetrics(1)
|
||||||
|
|
||||||
if fullscreen:
|
if fullscreen:
|
||||||
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
|
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
|
||||||
@ -287,6 +303,9 @@ class Display:
|
|||||||
else:
|
else:
|
||||||
self.current_presentation.prev()
|
self.current_presentation.prev()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
|
elif self.config.REVERSE.match(key):
|
||||||
|
self.current_presentation.reverse_slide()
|
||||||
|
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_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
@ -296,7 +315,6 @@ class Display:
|
|||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--folder",
|
"--folder",
|
||||||
@ -304,20 +322,26 @@ class Display:
|
|||||||
type=click.Path(exists=True, file_okay=False),
|
type=click.Path(exists=True, file_okay=False),
|
||||||
help="Set slides folder.",
|
help="Set slides folder.",
|
||||||
)
|
)
|
||||||
"""
|
|
||||||
@click.help_option("-h", "--help")
|
@click.help_option("-h", "--help")
|
||||||
def list_scenes(folder):
|
def list_scenes(folder):
|
||||||
|
"""List available scenes."""
|
||||||
|
|
||||||
|
for i, scene in enumerate(_list_scenes(folder), start=1):
|
||||||
|
click.secho(f"{i}: {scene}", fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
def _list_scenes(folder):
|
||||||
scenes = []
|
scenes = []
|
||||||
|
|
||||||
for file in os.listdir(folder):
|
for file in os.listdir(folder):
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
scenes.append(os.path.basename(file)[:-4])
|
scenes.append(os.path.basename(file)[:-5])
|
||||||
|
|
||||||
return scenes
|
return scenes
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--scenes", nargs=-1, prompt=True)
|
@click.argument("scenes", nargs=-1)
|
||||||
@config_path_option
|
@config_path_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--folder",
|
"--folder",
|
||||||
@ -337,19 +361,36 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
|
|||||||
"""Present the different scenes."""
|
"""Present the different scenes."""
|
||||||
|
|
||||||
if len(scenes) == 0:
|
if len(scenes) == 0:
|
||||||
print("ICI")
|
scene_choices = _list_scenes(folder)
|
||||||
scene_choices = list_scenes(folder)
|
|
||||||
|
|
||||||
scene_choices = dict(enumerate(scene_choices, start=1))
|
scene_choices = dict(enumerate(scene_choices, start=1))
|
||||||
choices = [str(i) for i in scene_choices.keys()]
|
|
||||||
|
for i, scene in scene_choices.items():
|
||||||
|
click.secho(f"{i}: {scene}", fg="green")
|
||||||
|
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
click.echo("Choose number corresponding to desired scene/arguments.")
|
||||||
|
click.echo("(Use comma separated list for multiple entries)")
|
||||||
|
|
||||||
def value_proc(value: str):
|
def value_proc(value: str):
|
||||||
raise ValueError("Value:")
|
indices = list(map(int, value.strip().replace(" ", "").split(",")))
|
||||||
|
|
||||||
print(scene_choices)
|
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
|
||||||
|
raise ValueError("Please only enter numbers displayed on the screen.")
|
||||||
scenes = click.prompt("Choose a scene", value_proc=value_proc)
|
|
||||||
|
return [scene_choices[i] for i in indices]
|
||||||
|
|
||||||
|
if len(scene_choices) == 0:
|
||||||
|
raise ValueError("No scenes were found, are you in the correct directory?")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||||
|
break
|
||||||
|
except ValueError as e:
|
||||||
|
click.secho(e, fg="red")
|
||||||
|
|
||||||
presentations = list()
|
presentations = list()
|
||||||
for scene in scenes:
|
for scene in scenes:
|
||||||
config_file = os.path.join(folder, f"{scene}.json")
|
config_file = os.path.join(folder, f"{scene}.json")
|
||||||
|
@ -91,11 +91,12 @@ class Slide(Scene):
|
|||||||
scene_name = type(self).__name__
|
scene_name = type(self).__name__
|
||||||
scene_files_folder = os.path.join(files_folder, scene_name)
|
scene_files_folder = os.path.join(files_folder, scene_name)
|
||||||
|
|
||||||
if os.path.exists(scene_files_folder):
|
old_animation_files = set()
|
||||||
shutil.rmtree(scene_files_folder)
|
|
||||||
|
|
||||||
if not os.path.exists(scene_files_folder):
|
if not os.path.exists(scene_files_folder):
|
||||||
os.mkdir(scene_files_folder)
|
os.mkdir(scene_files_folder)
|
||||||
|
else:
|
||||||
|
old_animation_files.update(os.listdir(scene_files_folder))
|
||||||
|
|
||||||
files = list()
|
files = list()
|
||||||
for src_file in tqdm(
|
for src_file in tqdm(
|
||||||
@ -105,10 +106,25 @@ class Slide(Scene):
|
|||||||
ascii=True if platform.system() == "Windows" else None,
|
ascii=True if platform.system() == "Windows" else None,
|
||||||
disable=config["progress_bar"] == "none",
|
disable=config["progress_bar"] == "none",
|
||||||
):
|
):
|
||||||
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file))
|
filename = os.path.basename(src_file)
|
||||||
shutil.copyfile(src_file, dst_file)
|
_hash, ext = os.path.splitext(filename)
|
||||||
rev_file = reverse_video_path(dst_file)
|
|
||||||
reverse_video_file(src_file, rev_file)
|
rev_filename = f"{_hash}_reversed{ext}"
|
||||||
|
|
||||||
|
dst_file = os.path.join(scene_files_folder, filename)
|
||||||
|
# We only copy animation if it was not present
|
||||||
|
if filename in old_animation_files:
|
||||||
|
old_animation_files.remove(filename)
|
||||||
|
else:
|
||||||
|
shutil.copyfile(src_file, dst_file)
|
||||||
|
|
||||||
|
# We only reverse video if it was not present
|
||||||
|
if rev_filename in old_animation_files:
|
||||||
|
old_animation_files.remove(rev_filename)
|
||||||
|
else:
|
||||||
|
rev_file = os.path.join(scene_files_folder, rev_filename)
|
||||||
|
reverse_video_file(src_file, rev_file)
|
||||||
|
|
||||||
files.append(dst_file)
|
files.append(dst_file)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ import sys
|
|||||||
|
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
from .__version__ import __version__ as version
|
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+")
|
||||||
|
Reference in New Issue
Block a user