mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 11:05:54 +08:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
d1c3e9c075 | |||
38e03db9e9 | |||
2eafa0b82e | |||
8a4fea687d | |||
128d6718ae | |||
22cbb7ec94 | |||
5feb13da10 | |||
90b2e4d46b | |||
43b9fa7cf7 | |||
1cc070db86 | |||
c5274fb57f | |||
2a136ed585 | |||
c9390f0e59 | |||
41de205675 | |||
133ec17ebb | |||
09199777e0 | |||
bfcf7db26e | |||
b6522f4756 | |||
4216299b39 |
@ -46,6 +46,8 @@ class Example(Slide):
|
||||
self.wait()
|
||||
```
|
||||
|
||||
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(..)`
|
||||
|
||||
To start the presentation using `Scene1`, `Scene2` and so on simply run:
|
||||
|
||||
```
|
||||
@ -64,6 +66,10 @@ Default keybindings to control the presentation
|
||||
| Spacebar | Play/Pause |
|
||||
| Q | Quit |
|
||||
|
||||
|
||||
You can specify different keybindings creating a file named `manim-presentation.json` with the keys: `QUIT_KEY` `CONTINUE_KEY` `BACK_KEY` `REWIND_KEY` and `PLAYPAUSE_KEY`
|
||||
`manim-presentation` uses `cv2.waitKeyEx()` to wait for keypresses
|
||||
|
||||
## Run Example
|
||||
|
||||
Clone this repository
|
||||
|
11
example.py
11
example.py
@ -16,4 +16,13 @@ class Example(Slide):
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
self.pause()
|
||||
|
||||
self.wait()
|
||||
self.play(dot.animate.move_to(RIGHT*3))
|
||||
self.pause()
|
||||
|
||||
self.start_loop()
|
||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||
self.end_loop()
|
||||
|
||||
# Each slide MUST end with an animation (a self.wait is considered an animation)
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
|
@ -7,13 +7,28 @@ import math
|
||||
import time
|
||||
import argparse
|
||||
from enum import Enum
|
||||
import platform
|
||||
|
||||
class Config:
|
||||
QUIT_KEY = ord("q")
|
||||
CONTINUE_KEY = 83 #right arrow
|
||||
BACK_KEY = 81 #left arrow
|
||||
REWIND_KEY = ord("r")
|
||||
PLAYPAUSE_KEY = 32 #spacebar
|
||||
@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
|
||||
|
||||
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
|
||||
@ -35,44 +50,54 @@ def fix_time(x):
|
||||
return x if x > 0 else 1
|
||||
|
||||
class Presentation:
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, last_frame_next=False):
|
||||
self.last_frame_next = last_frame_next
|
||||
self.slides = config["slides"]
|
||||
self.files = config["files"]
|
||||
|
||||
self.lastframe = []
|
||||
|
||||
self.reset()
|
||||
self.load_files()
|
||||
self.add_last_slide()
|
||||
|
||||
def reset(self):
|
||||
self.current_animation = 0
|
||||
self.current_slide_i = 0
|
||||
|
||||
|
||||
def add_last_slide(self):
|
||||
last_slide_end = self.slides[-1]["end_animation"]
|
||||
last_animation = len(self.files) - 1
|
||||
last_animation = len(self.files)
|
||||
self.slides.append(dict(
|
||||
start_animation = last_slide_end,
|
||||
end_animation = last_animation,
|
||||
type = "last",
|
||||
number = len(self.slides) + 1
|
||||
number = len(self.slides) + 1,
|
||||
terminated = False
|
||||
))
|
||||
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.current_animation = 0
|
||||
self.current_slide_i = 0
|
||||
self.slides[-1]["terminated"] = False
|
||||
|
||||
def load_files(self):
|
||||
self.caps = list()
|
||||
for f in self.files:
|
||||
self.caps.append(cv2.VideoCapture(f))
|
||||
|
||||
def next(self):
|
||||
self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1)
|
||||
self.current_animation = self.current_slide["start_animation"]
|
||||
if self.current_slide["type"] == "last":
|
||||
self.current_slide["terminated"] = True
|
||||
else:
|
||||
self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1)
|
||||
self.rewind_slide()
|
||||
|
||||
def prev(self):
|
||||
self.current_slide_i = max(0, self.current_slide_i - 1)
|
||||
self.current_animation = self.current_slide["start_animation"]
|
||||
self.rewind_slide()
|
||||
|
||||
def rewind(self):
|
||||
def rewind_slide(self):
|
||||
self.current_animation = self.current_slide["start_animation"]
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
@property
|
||||
def current_slide(self):
|
||||
@ -86,30 +111,51 @@ class Presentation:
|
||||
def fps(self):
|
||||
return self.current_cap.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
def get_frame_and_state(self):
|
||||
ret, frame = self.current_cap.read()
|
||||
state = State.PLAYING
|
||||
if ret:
|
||||
# This function updates the state given the previous state.
|
||||
# It does this by reading the video information and checking if the state is still correct.
|
||||
# It returns the frame to show (lastframe) and the new state.
|
||||
def update_state(self, state):
|
||||
if state == State.PAUSED:
|
||||
if len(self.lastframe) == 0:
|
||||
_, self.lastframe = self.current_cap.read()
|
||||
return self.lastframe, state
|
||||
still_playing, frame = self.current_cap.read()
|
||||
if still_playing:
|
||||
self.lastframe = frame
|
||||
else:
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
elif state in [state.WAIT, state.PAUSED]:
|
||||
return self.lastframe, state
|
||||
elif self.current_slide["type"] == "last" and self.current_slide["terminated"]:
|
||||
return self.lastframe, State.END
|
||||
|
||||
if not still_playing:
|
||||
if self.current_slide["end_animation"] == self.current_animation + 1:
|
||||
if self.current_slide["type"] == "slide":
|
||||
# To fix "it always ends one frame before the animation", uncomment this.
|
||||
# But then clears on the next slide will clear the stationary after this slide.
|
||||
if self.last_frame_next:
|
||||
self.next_cap = self.caps[self.current_animation + 1]
|
||||
self.next_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
_, self.lastframe = self.next_cap.read()
|
||||
state = State.WAIT
|
||||
elif self.current_slide["type"] == "loop":
|
||||
self.current_animation = self.current_slide["start_animation"]
|
||||
state = State.PLAYING
|
||||
self.rewind_slide()
|
||||
elif self.current_slide["type"] == "last":
|
||||
return self.lastframe, State.END
|
||||
self.current_slide["terminated"] = True
|
||||
elif self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation:
|
||||
return self.lastframe, State.END
|
||||
state = State.WAIT
|
||||
else:
|
||||
# Play next video!
|
||||
self.current_animation += 1
|
||||
# Reset video to position zero if it has been played before
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
return self.lastframe, state
|
||||
|
||||
|
||||
class Display:
|
||||
def __init__(self, presentations, start_paused=False):
|
||||
def __init__(self, presentations, start_paused=False, fullscreen=False):
|
||||
self.presentations = presentations
|
||||
self.start_paused = start_paused
|
||||
|
||||
@ -120,14 +166,18 @@ class Display:
|
||||
self.lag = 0
|
||||
self.last_time = now()
|
||||
|
||||
if fullscreen:
|
||||
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
|
||||
cv2.setWindowProperty("Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
|
||||
|
||||
@property
|
||||
def current_presentation(self):
|
||||
return self.presentations[self.current_presentation_i]
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.state == State.PLAYING:
|
||||
self.lastframe, self.state = self.current_presentation.get_frame_and_state()
|
||||
self.lastframe, self.state = self.current_presentation.update_state(self.state)
|
||||
if self.state == State.PLAYING or self.state == State.PAUSED:
|
||||
if self.start_paused:
|
||||
self.state = State.PAUSED
|
||||
self.start_paused = False
|
||||
@ -137,10 +187,9 @@ class Display:
|
||||
else:
|
||||
self.current_presentation_i += 1
|
||||
self.state = State.PLAYING
|
||||
|
||||
self.handle_key()
|
||||
self.show_video()
|
||||
self.show_info()
|
||||
self.handle_key()
|
||||
|
||||
def show_video(self):
|
||||
self.lag = now() - self.last_time
|
||||
@ -190,7 +239,7 @@ class Display:
|
||||
|
||||
def handle_key(self):
|
||||
sleep_time = math.ceil(1000/self.current_presentation.fps)
|
||||
key = cv2.waitKey(fix_time(sleep_time - self.lag)) & 0xFF
|
||||
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
|
||||
|
||||
if key == Config.QUIT_KEY:
|
||||
self.quit()
|
||||
@ -212,7 +261,7 @@ class Display:
|
||||
self.current_presentation.prev()
|
||||
self.state = State.PLAYING
|
||||
elif key == Config.REWIND_KEY:
|
||||
self.current_presentation.rewind()
|
||||
self.current_presentation.rewind_slide()
|
||||
self.state = State.PLAYING
|
||||
|
||||
|
||||
@ -227,9 +276,13 @@ def main():
|
||||
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()
|
||||
|
||||
presentations = list()
|
||||
for scene in args.scenes:
|
||||
@ -237,9 +290,9 @@ def main():
|
||||
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))
|
||||
presentations.append(Presentation(config, last_frame_next=args.last_frame_next))
|
||||
|
||||
display = Display(presentations, start_paused=args.start_paused)
|
||||
display = Display(presentations, start_paused=args.start_paused, fullscreen=args.fullscreen)
|
||||
display.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -2,6 +2,7 @@ import os
|
||||
import json
|
||||
import shutil
|
||||
from manim import Scene, config
|
||||
from manim.animation.animation import Wait
|
||||
|
||||
class Slide(Scene):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
2
setup.py
2
setup.py
@ -6,7 +6,7 @@ with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "README.md")
|
||||
|
||||
setuptools.setup(
|
||||
name="manim_presentation",
|
||||
version="0.1.3",
|
||||
version="0.2.0",
|
||||
author="Federico A. Galatolo",
|
||||
author_email="federico.galatolo@ing.unipi.it",
|
||||
description="Tool for live presentations using manim",
|
||||
|
Reference in New Issue
Block a user