Skip to content

Commit 9640605

Browse files
committed
feat(cli): reverse videos on the fly
2 parents efbe488 + 3520c42 commit 9640605

File tree

7 files changed

+94
-34
lines changed

7 files changed

+94
-34
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Default keybindings to control the presentation:
6565
| Right Arrow | Continue/Next Slide |
6666
| Left Arrow | Previous Slide |
6767
| R | Re-Animate Current Slide |
68+
| V | Reverse Current Slide |
6869
| Spacebar | Play/Pause |
6970
| Q | Quit |
7071

@@ -122,10 +123,10 @@ Here are a few things that I implemented (or that I'm planning to implement) on
122123
- [x] Only one cli (to rule them all)
123124
- [x] User can easily generate dummy config file
124125
- [x] Config file path can be manually set
125-
- [ ] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
126+
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
126127
- [x] Handle 3D scenes out of the box
127128
- [ ] Generate docs online
128-
- [ ] Fix the quality problem on Windows platforms with `fullscreen` flag
129+
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
129130

130131
## Contributions and license
131132

manim_slides/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.0.1"
1+
__version__ = "3.1.0"

manim_slides/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Config(BaseModel):
2323
QUIT: Key = Key(ids=[ord("q")], name="QUIT")
2424
CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT")
2525
BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK")
26+
REVERSE: Key = Key(ids=[ord("v")], name="REVERSE")
2627
REWIND: Key = Key(ids=[ord("r")], name="REWIND")
2728
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
2829

@@ -33,7 +34,7 @@ def ids_are_unique_across_keys(cls, values):
3334
for key in values.values():
3435
if len(ids.intersection(key.ids)) != 0:
3536
raise ValueError(
36-
f"Two or more keys share a common key code: please make sure each key has distinc key codes"
37+
"Two or more keys share a common key code: please make sure each key has distinc key codes"
3738
)
3839
ids.update(key.ids)
3940

manim_slides/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from click_default_group import DefaultGroup
33

44
from . import __version__
5-
from .present import present
5+
from .present import list_scenes, present
66
from .wizard import init, wizard
77

88

@@ -13,6 +13,7 @@ def cli():
1313
pass
1414

1515

16+
cli.add_command(list_scenes)
1617
cli.add_command(present)
1718
cli.add_command(wizard)
1819
cli.add_command(init)

manim_slides/present.py

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ class Presentation:
4242
def __init__(self, config, last_frame_next: bool = False):
4343
self.last_frame_next = last_frame_next
4444
self.slides = config["slides"]
45-
self.files = [reverse_video_path(path) for path in config["files"]]
45+
self.files = [path for path in config["files"]]
46+
self.reverse = False
47+
self.reversed_slide = -1
4648

4749
self.lastframe = []
4850

@@ -80,22 +82,34 @@ def prev(self):
8082
self.current_slide_i = max(0, self.current_slide_i - 1)
8183
self.rewind_slide()
8284

83-
def reserve_slide(self):
84-
pass
85+
def reverse_slide(self):
86+
self.rewind_slide(reverse=True)
8587

86-
def rewind_slide(self):
88+
def rewind_slide(self, reverse: bool = False):
89+
self.reverse = reverse
8790
self.current_animation = self.current_slide["start_animation"]
8891
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
8992

90-
def load_this_cap(self, cap_number):
91-
if self.caps[cap_number] == None:
93+
def load_this_cap(self, cap_number: int):
94+
if (
95+
self.caps[cap_number] is None
96+
or (self.reverse and self.reversed_slide != cap_number)
97+
or (not self.reverse and self.reversed_slide == cap_number)
98+
):
9299
# unload other caps
93100
for i in range(len(self.caps)):
94-
if self.caps[i] != None:
101+
if not self.caps[i] is None:
95102
self.caps[i].release()
96103
self.caps[i] = None
97104
# load this cap
98-
self.caps[cap_number] = cv2.VideoCapture(self.files[cap_number])
105+
file = self.files[cap_number]
106+
if self.reverse:
107+
self.reversed_slide = cap_number
108+
file = "{}_reversed{}".format(*os.path.splitext(file))
109+
else:
110+
self.reversed_slide = -1
111+
112+
self.caps[cap_number] = cv2.VideoCapture(file)
99113

100114
@property
101115
def current_slide(self):
@@ -174,7 +188,9 @@ def __init__(self, presentations, config, start_paused=False, fullscreen=False):
174188

175189
if platform.system() == "Windows":
176190
user32 = ctypes.windll.user32
177-
self.screen_width, self.screen_height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)
191+
self.screen_width, self.screen_height = user32.GetSystemMetrics(
192+
0
193+
), user32.GetSystemMetrics(1)
178194

179195
if fullscreen:
180196
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
@@ -287,6 +303,9 @@ def handle_key(self):
287303
else:
288304
self.current_presentation.prev()
289305
self.state = State.PLAYING
306+
elif self.config.REVERSE.match(key):
307+
self.current_presentation.reverse_slide()
308+
self.state = State.PLAYING
290309
elif self.config.REWIND.match(key):
291310
self.current_presentation.rewind_slide()
292311
self.state = State.PLAYING
@@ -296,28 +315,33 @@ def quit(self):
296315
sys.exit()
297316

298317

299-
"""
300318
@click.command()
301319
@click.option(
302320
"--folder",
303321
default=FOLDER_PATH,
304322
type=click.Path(exists=True, file_okay=False),
305323
help="Set slides folder.",
306324
)
307-
"""
308325
@click.help_option("-h", "--help")
309326
def list_scenes(folder):
327+
"""List available scenes."""
328+
329+
for i, scene in enumerate(_list_scenes(folder), start=1):
330+
click.secho(f"{i}: {scene}", fg="green")
331+
332+
333+
def _list_scenes(folder):
310334
scenes = []
311335

312336
for file in os.listdir(folder):
313337
if file.endswith(".json"):
314-
scenes.append(os.path.basename(file)[:-4])
338+
scenes.append(os.path.basename(file)[:-5])
315339

316340
return scenes
317341

318342

319343
@click.command()
320-
@click.option("--scenes", nargs=-1, prompt=True)
344+
@click.argument("scenes", nargs=-1)
321345
@config_path_option
322346
@click.option(
323347
"--folder",
@@ -337,19 +361,36 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
337361
"""Present the different scenes."""
338362

339363
if len(scenes) == 0:
340-
print("ICI")
341-
scene_choices = list_scenes(folder)
364+
scene_choices = _list_scenes(folder)
342365

343366
scene_choices = dict(enumerate(scene_choices, start=1))
344-
choices = [str(i) for i in scene_choices.keys()]
367+
368+
for i, scene in scene_choices.items():
369+
click.secho(f"{i}: {scene}", fg="green")
370+
371+
click.echo()
372+
373+
click.echo("Choose number corresponding to desired scene/arguments.")
374+
click.echo("(Use comma separated list for multiple entries)")
345375

346376
def value_proc(value: str):
347-
raise ValueError("Value:")
348-
349-
print(scene_choices)
350-
351-
scenes = click.prompt("Choose a scene", value_proc=value_proc)
352-
377+
indices = list(map(int, value.strip().replace(" ", "").split(",")))
378+
379+
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
380+
raise ValueError("Please only enter numbers displayed on the screen.")
381+
382+
return [scene_choices[i] for i in indices]
383+
384+
if len(scene_choices) == 0:
385+
raise ValueError("No scenes were found, are you in the correct directory?")
386+
387+
while True:
388+
try:
389+
scenes = click.prompt("Choice(s)", value_proc=value_proc)
390+
break
391+
except ValueError as e:
392+
click.secho(e, fg="red")
393+
353394
presentations = list()
354395
for scene in scenes:
355396
config_file = os.path.join(folder, f"{scene}.json")

manim_slides/slide.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,12 @@ def render(self, *args, **kwargs):
9191
scene_name = type(self).__name__
9292
scene_files_folder = os.path.join(files_folder, scene_name)
9393

94-
if os.path.exists(scene_files_folder):
95-
shutil.rmtree(scene_files_folder)
94+
old_animation_files = set()
9695

9796
if not os.path.exists(scene_files_folder):
9897
os.mkdir(scene_files_folder)
98+
else:
99+
old_animation_files.update(os.listdir(scene_files_folder))
99100

100101
files = list()
101102
for src_file in tqdm(
@@ -105,10 +106,25 @@ def render(self, *args, **kwargs):
105106
ascii=True if platform.system() == "Windows" else None,
106107
disable=config["progress_bar"] == "none",
107108
):
108-
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file))
109-
shutil.copyfile(src_file, dst_file)
110-
rev_file = reverse_video_path(dst_file)
111-
reverse_video_file(src_file, rev_file)
109+
filename = os.path.basename(src_file)
110+
_hash, ext = os.path.splitext(filename)
111+
112+
rev_filename = f"{_hash}_reversed{ext}"
113+
114+
dst_file = os.path.join(scene_files_folder, filename)
115+
# We only copy animation if it was not present
116+
if filename in old_animation_files:
117+
old_animation_files.remove(filename)
118+
else:
119+
shutil.copyfile(src_file, dst_file)
120+
121+
# We only reverse video if it was not present
122+
if rev_filename in old_animation_files:
123+
old_animation_files.remove(rev_filename)
124+
else:
125+
rev_file = os.path.join(scene_files_folder, rev_filename)
126+
reverse_video_file(src_file, rev_file)
127+
112128
files.append(dst_file)
113129

114130
logger.info(

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import setuptools
44

5-
from .__version__ import __version__ as version
5+
from manim_slides import __version__ as version
66

77
if sys.version_info < (3, 7):
88
raise RuntimeError("This package requires Python 3.7+")

0 commit comments

Comments
 (0)