Compare commits

...

38 Commits

Author SHA1 Message Date
0d97bdabb8 release new version 2022-09-11 12:26:46 +02:00
8e50adf0ba Merge pull request #8 from jeertmans/fix-shape
fix(bug): allows shape to be 2 or 3 dims
2022-09-11 12:26:01 +02:00
d583d591c1 fix(bug): allows shape to be 2 or 3 dims 2022-09-11 12:25:09 +02:00
c243fe3481 fix: add tqdm to setup 2022-09-11 12:14:54 +02:00
eba84a44fc fix(deps): remove manim dep 2022-09-10 23:10:42 +02:00
c8cf2e6074 Update README.md 2022-09-10 23:06:30 +02:00
743329c043 release 3.2 2022-09-10 23:00:26 +02:00
ed1b2eb698 Merge pull request #6 from jeertmans/manimgl
feat: add support for manimgl
2022-09-10 22:59:34 +02:00
cda304fef0 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-09-10 20:54:37 +00:00
3e6c562b1d Merge remote-tracking branch 'origin/manimgl' into manimgl 2022-09-10 22:54:29 +02:00
85c295a2c1 feat: add support for manimgl 2022-09-10 22:54:27 +02:00
ac5582753c feat: add support for manimgl 2022-09-10 22:53:35 +02:00
c53e410ff8 Update README.md 2022-09-08 00:13:01 +02:00
e7c2bcfe98 Merge pull request #5 from jeertmans/reserve-video-and-more
feat(cli): reverse videos on the fly!
2022-09-08 00:07:27 +02:00
ac486f4f23 trying to fix builds 2022-09-08 00:03:47 +02:00
0bd29202e7 Revert "remove builds"
This reverts commit 78941e10fcd1e612333f4aab51faf6f3dc4a63b6.
2022-09-08 00:02:41 +02:00
061706922b Revert "remove dist"
This reverts commit dab96c980d1e9f1f2fb6f7d98b975330fa7841f5.
2022-09-08 00:02:35 +02:00
574c545f86 Merge pull request #4 from jeertmans/reserve-video-and-more
feat(cli): reverse videos on the fly!
2022-09-07 23:49:26 +02:00
dab96c980d remove dist 2022-09-07 23:48:49 +02:00
78941e10fc remove builds 2022-09-07 23:48:16 +02:00
255c86ba25 fix: manual install 2022-09-07 23:45:20 +02:00
981b34faf5 Merge remote-tracking branch 'origin/reserve-video-and-more' into reserve-video-and-more 2022-09-07 23:44:13 +02:00
cc42a1bac5 fix: CI 2022-09-07 23:44:08 +02:00
efa6fe05cf [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-09-07 21:40:31 +00:00
11494209e4 fix: import errors... 2022-09-07 23:40:05 +02:00
e0669707e7 Merge pull request #3 from jeertmans/reserve-video-and-more
feat(cli): reverse videos on the fly!
2022-09-07 23:36:03 +02:00
51ca828920 fix: typo 2022-09-07 23:35:40 +02:00
2076d65944 fix(deps): fix deps import error 2022-09-07 23:34:33 +02:00
9640605ae9 feat(cli): reverse videos on the fly 2022-09-07 23:24:54 +02:00
efbe488660 WIP: reversing videos, etc. 2022-09-07 20:04:07 +02:00
e82ab99186 Merge pull request #2 from jeertmans/windows-fix
fix(windows): enhance resolution in fullscreen mode
2022-09-07 20:03:04 +02:00
7128c01ab5 fix(hooks): reset black fmt 2022-08-26 08:55:22 +02:00
c6d44b0b3b fix(cli): add '.' after command description 2022-08-26 08:54:18 +02:00
14b334fa6f fix(windows): enhance resolution in fullscreen mode 2022-08-02 16:59:42 +02:00
68858c3c33 Merge pull request #1 from jeertmans/ThreeDSlide
Add ThreeDSlides support
2022-08-02 16:20:28 +02:00
3520c42d7c WIP: reversing videos, etc. 2022-07-18 12:38:06 +02:00
1720a7d742 Working 3DScene example 2022-07-13 23:04:12 +02:00
d9eab15fa5 WIP: ThreeDSlide 2022-07-13 18:00:11 +02:00
13 changed files with 428 additions and 113 deletions

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
@ -21,6 +21,12 @@ jobs:
# Used to host cibuildwheel
- uses: actions/setup-python@v2
- name: Install deps
run: sudo apt-get install libsdl-pango-dev
- name: Install packages
run: python -m pip install -U manim tqdm
- name: Install cibuildwheel
run: python -m pip install -U setuptools wheel pip

4
.gitignore vendored
View File

@ -13,3 +13,7 @@ __pycache__/
slides/
.manim-slides.json
videos/
images/

View File

@ -5,6 +5,10 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:

View File

@ -3,9 +3,9 @@
![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides)
# Manim Slides
Tool for live presentations using either [manim](http://3b1b.github.io/manim/) or [manim-community](https://www.manim.community/).
Tool for live presentations using either [manim-community](https://www.manim.community/) or [manimgl](https://3b1b.github.io/manim/). `manim-slides` will automatically detect the one you are using!
> **_NOTE:_** This project is a fork of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation). Since the project seemed to be inactive, I decided to create my own fork to deploy new features more rapidly.
> **_NOTE:_** This project extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
## Install
@ -29,6 +29,7 @@ call `self.pause()` when you want to pause the playback and wait for an input to
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
```python
from manim import *
# or: from manimlib import *
from manim_slides import Slide
class Example(Slide):
@ -65,6 +66,7 @@ Default keybindings to control the presentation:
| Right Arrow | Continue/Next Slide |
| Left Arrow | Previous Slide |
| R | Re-Animate Current Slide |
| V | Reverse Current Slide |
| Spacebar | Play/Pause |
| Q | Quit |
@ -75,7 +77,7 @@ You can run the **configuration wizard** with:
manim-slides wizard
```
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REWIND` and `PLAY_PAUSE`.
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REVERSE` `REWIND` and `PLAY_PAUSE`.
A default file can be created with:
```
@ -95,11 +97,15 @@ cd manim-slides
Install `manim` and `manim-slides`:
```
pip install manim manim-slides
# or
pip install manimgl manim-slides
```
Render the example scene:
```
manim -qh example.py
manim -qh example.py Example
# or
manimgl --hd example.py Example
```
Run the presentation
@ -122,11 +128,11 @@ 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] User can easily generate dummy config file
- [x] Config file path can be manually set
- [ ] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
- [ ] Handle 3D scenes out of the box
- [ ] Can work with both community and 3b1b versions (not tested)
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
- [x] Handle 3D scenes out of the box
- [x] Support for both `manim` and `manimgl` modules
- [ ] 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

View File

@ -1,4 +1,6 @@
from manim import *
# If you want to use manimgl, uncomment change
# manim to manimlib
from manimlib import *
from manim_slides import Slide, ThreeDSlide
@ -28,25 +30,38 @@ class Example(Slide):
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle=Circle()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.add(axes)
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
self.add(circle,axes)
self.begin_ambient_camera_rotation(rate=0.1)
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.wait()
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.start_loop()
self.play(circle.animate.move_to(ORIGIN + UP))
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.play(circle.animate.move_to(ORIGIN))
# TODO: fixit
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))

View File

@ -1 +1 @@
__version__ = "3.0.1"
__version__ = "3.2.2"

View File

@ -23,6 +23,7 @@ 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")
REVERSE: Key = Key(ids=[ord("v")], name="REVERSE")
REWIND: Key = Key(ids=[ord("r")], name="REWIND")
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
@ -33,7 +34,7 @@ class Config(BaseModel):
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"
"Two or more keys share a common key code: please make sure each key has distinc key codes"
)
ids.update(key.ids)

View File

@ -2,7 +2,7 @@ import click
from click_default_group import DefaultGroup
from . import __version__
from .present import present
from .present import list_scenes, present
from .wizard import init, wizard
@ -12,6 +12,8 @@ from .wizard import init, wizard
def cli():
pass
cli.add_command(list_scenes)
cli.add_command(present)
cli.add_command(wizard)
cli.add_command(init)

45
manim_slides/manim.py Normal file
View File

@ -0,0 +1,45 @@
import sys
from importlib.util import find_spec
MANIM_PACKAGE_NAME = "manim"
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
MANIMGL_PACKAGE_NAME = "manimlib"
MANIMGL_AVAILABLE = find_spec(MANIMGL_PACKAGE_NAME) is not None
MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
if MANIM_IMPORTED and MANIMGL_IMPORTED:
from manim import logger
logger.warn(
"Both manim and manimgl are installed, therefore `manim-slide` needs to need which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
)
MANIM = True
MANIMGL = False
elif MANIM_AVAILABLE and not MANIMGL_IMPORTED:
MANIM = True
MANIMGL = False
elif MANIMGL_AVAILABLE:
MANIM = False
MANIMGL = True
else:
raise ImportError(
"Either manim (community) or manimgl (3b1b) package must be installed"
)
FFMPEG_BIN = None
if MANIMGL:
from manimlib import Scene, ThreeDScene, config
from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log as logger
else:
from manim import Scene, ThreeDScene, config, logger
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable

View File

@ -1,9 +1,13 @@
import json
import math
import os
import platform
import sys
import time
from enum import Enum
from enum import IntEnum, auto, unique
if platform.system() == "Windows":
import ctypes
import click
import cv2
@ -12,32 +16,35 @@ import numpy as np
from .commons import config_path_option
from .config import Config
from .defaults import CONFIG_PATH, FOLDER_PATH
from .slide import reverse_video_path
class State(Enum):
PLAYING = 0
PAUSED = 1
WAIT = 2
END = 3
@unique
class State(IntEnum):
PLAYING = auto()
PAUSED = auto()
WAIT = auto()
END = auto()
def __str__(self):
if self.value == 0: return "Playing"
if self.value == 1: return "Paused"
if self.value == 2: return "Wait"
if self.value == 3: return "End"
return "..."
return self.name.capitalize()
def now():
def now() -> int:
return round(time.time() * 1000)
def fix_time(x):
def fix_time(x: float) -> float:
return x if x > 0 else 1
class Presentation:
def __init__(self, config, last_frame_next: bool = False):
self.last_frame_next = last_frame_next
self.slides = config["slides"]
self.files = config["files"]
self.files = [path for path in config["files"]]
self.reverse = False
self.reversed_slide = -1
self.lastframe = []
@ -48,14 +55,15 @@ class Presentation:
def add_last_slide(self):
last_slide_end = self.slides[-1]["end_animation"]
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,
terminated = False
))
self.slides.append(
dict(
start_animation=last_slide_end,
end_animation=last_animation,
type="last",
number=len(self.slides) + 1,
terminated=False,
)
)
def reset(self):
self.current_animation = 0
@ -74,19 +82,34 @@ class Presentation:
self.current_slide_i = max(0, self.current_slide_i - 1)
self.rewind_slide()
def rewind_slide(self):
def reverse_slide(self):
self.rewind_slide(reverse=True)
def rewind_slide(self, reverse: bool = False):
self.reverse = reverse
self.current_animation = self.current_slide["start_animation"]
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def load_this_cap(self,cap_number):
if self.caps[cap_number] == None:
def load_this_cap(self, cap_number: int):
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
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] = None
# 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
def current_slide(self):
@ -135,7 +158,10 @@ class Presentation:
self.rewind_slide()
elif self.current_slide["type"] == "last":
self.current_slide["terminated"] = True
elif self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation:
elif (
self.current_slide["type"] == "last"
and self.current_slide["end_animation"] == self.current_animation
):
state = State.WAIT
else:
# Play next video!
@ -160,9 +186,27 @@ class Display:
self.lag = 0
self.last_time = now()
if platform.system() == "Windows":
user32 = ctypes.windll.user32
self.screen_width, self.screen_height = user32.GetSystemMetrics(
0
), user32.GetSystemMetrics(1)
if fullscreen:
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty("Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
cv2.setWindowProperty(
"Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
)
def resize_frame_to_screen(self, frame: np.ndarray):
frame_height, frame_width = frame.shape[:2]
scale_height = self.screen_height / frame_height
scale_width = self.screen_width / frame_width
scale = min(scale_height, scale_width)
return cv2.resize(frame, (int(scale * frame_height, scale * frame_width)))
@property
def current_presentation(self):
@ -170,7 +214,9 @@ class Display:
def run(self):
while True:
self.lastframe, self.state = self.current_presentation.update_state(self.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
@ -188,7 +234,13 @@ class Display:
def show_video(self):
self.lag = now() - self.last_time
self.last_time = now()
cv2.imshow("Video", self.lastframe)
frame = self.lastframe
if platform.system() == "Windows":
frame = self.resize_frame_to_screen(frame)
cv2.imshow("Video", frame)
def show_info(self):
info = np.zeros((130, 420), np.uint8)
@ -200,39 +252,34 @@ class Display:
info,
f"Animation: {self.current_presentation.current_animation}",
(grid_x[0], grid_y[0]),
*font_args
)
cv2.putText(
info,
f"State: {self.state}",
(grid_x[1], grid_y[0]),
*font_args
*font_args,
)
cv2.putText(info, f"State: {self.state}", (grid_x[1], grid_y[0]), *font_args)
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
(grid_x[0], grid_y[1]),
*font_args
*font_args,
)
cv2.putText(
info,
f"Slide Type: {self.current_presentation.current_slide['type']}",
(grid_x[1], grid_y[1]),
*font_args
*font_args,
)
cv2.putText(
info,
f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}",
((grid_x[0]+grid_x[1])//2, grid_y[2]),
*font_args
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
*font_args,
)
cv2.imshow("Info", info)
def handle_key(self):
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))
if self.config.QUIT.match(key):
@ -241,7 +288,9 @@ class Display:
self.state = State.PAUSED
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
self.state = State.PLAYING
elif self.state == State.WAIT and (self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)):
elif self.state == State.WAIT and (
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
):
self.current_presentation.next()
self.state = State.PLAYING
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
@ -254,32 +303,105 @@ class Display:
else:
self.current_presentation.prev()
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):
self.current_presentation.rewind_slide()
self.state = State.PLAYING
def quit(self):
cv2.destroyAllWindows()
sys.exit()
@click.command()
@click.option(
"--folder",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
)
@click.help_option("-h", "--help")
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 = []
for file in os.listdir(folder):
if file.endswith(".json"):
scenes.append(os.path.basename(file)[:-5])
return scenes
@click.command()
@click.argument("scenes", nargs=-1)
@config_path_option
@click.option("--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.")
@click.option(
"--folder",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
)
@click.option("--start-paused", is_flag=True, help="Start paused.")
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
@click.option("--last-frame-next", is_flag=True, help="Show the next animation first frame as last frame (hack).")
@click.option(
"--last-frame-next",
is_flag=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):
"""Present the different scenes"""
"""Present the different scenes."""
if len(scenes) == 0:
scene_choices = _list_scenes(folder)
scene_choices = dict(enumerate(scene_choices, start=1))
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):
indices = list(map(int, value.strip().replace(" ", "").split(",")))
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
raise click.UsageError(
"Please only enter numbers displayed on the screen."
)
return [scene_choices[i] for i in indices]
if len(scene_choices) == 0:
raise click.UsageError(
"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:
raise click.UsageError(e)
presentations = list()
for scene in scenes:
config_file = os.path.join(folder, f"{scene}.json")
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 click.UsageError(
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, last_frame_next=last_frame_next))
@ -288,5 +410,7 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
else:
config = Config()
display = Display(presentations, config=config, start_paused=start_paused, fullscreen=fullscreen)
display = Display(
presentations, config=config, start_paused=start_paused, fullscreen=fullscreen
)
display.run()

View File

@ -1,15 +1,41 @@
import json
import os
import platform
import shutil
import subprocess
from manim import Scene, ThreeDScene, config
from tqdm import tqdm
from .defaults import FOLDER_PATH
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
def reverse_video_path(src: str) -> str:
file, ext = os.path.splitext(src)
return f"{file}_reversed{ext}"
def reverse_video_file(src: str, dst: str):
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate()
class Slide(Scene):
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
super(Slide, self).__init__(*args, **kwargs)
if MANIMGL:
if not os.path.isdir("videos"):
os.mkdir("videos")
kwargs["file_writer_config"] = {
"break_into_partial_movies": True,
"output_directory": "",
"write_to_movie": True,
}
kwargs["preview"] = False
super().__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides = list()
self.current_slide = 1
@ -17,17 +43,48 @@ class Slide(Scene):
self.loop_start_animation = None
self.pause_start_animation = 0
@property
def partial_movie_files(self):
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
@property
def show_progress_bar(self):
if MANIMGL:
return getattr(super(Scene, self), "show_progress_bar", True)
else:
return config["progress_bar"] != "none"
@property
def leave_progress_bar(self):
if MANIMGL:
return getattr(super(Scene, self), "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
def play(self, *args, **kwargs):
super(Slide, self).play(*args, **kwargs)
super().play(*args, **kwargs)
self.current_animation += 1
def pause(self):
self.slides.append(dict(
type="slide",
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide
))
self.slides.append(
dict(
type="slide",
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
self.current_slide += 1
self.pause_start_animation = self.current_animation
@ -36,25 +93,22 @@ class Slide(Scene):
self.loop_start_animation = self.current_animation
def end_loop(self):
assert self.loop_start_animation is not None, "You have to start a loop before ending it"
self.slides.append(dict(
type="loop",
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide
))
assert (
self.loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
dict(
type="loop",
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
self.current_slide += 1
self.loop_start_animation = None
self.pause_start_animation = self.current_animation
def render(self, *args, **kwargs):
# We need to disable the caching limit since we rely on intermidiate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
super(Slide, self).render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
def save_slides(self, use_cache=True):
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
@ -66,24 +120,75 @@ class Slide(Scene):
scene_name = type(self).__name__
scene_files_folder = os.path.join(files_folder, scene_name)
if os.path.exists(scene_files_folder):
shutil.rmtree(scene_files_folder)
old_animation_files = set()
if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder)
elif not use_cache:
shutil.rmtree(scene_files_folder)
os.mkdir(scene_files_folder)
else:
old_animation_files.update(os.listdir(scene_files_folder))
files = list()
for src_file in self.renderer.file_writer.partial_movie_files:
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file))
shutil.copyfile(src_file, dst_file)
for src_file in tqdm(
self.partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=self.leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self.show_progress_bar,
):
filename = os.path.basename(src_file)
_hash, ext = os.path.splitext(filename)
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)
f = open(os.path.join(self.output_folder, "%s.json" % (scene_name, )), "w")
json.dump(dict(
slides=self.slides,
files=files
), f)
f.close()
logger.info(
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
)
class ThreeDSlide(ThreeDScene, Slide):
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
f = open(slide_path, "w")
json.dump(dict(slides=self.slides, files=files), f)
f.close()
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
)
def run(self, *args, **kwargs):
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
def render(self, *args, **kwargs):
"""MANIM render"""
# We need to disable the caching limit since we rely on intermidiate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
super().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
self.save_slides()
class ThreeDSlide(Slide, ThreeDScene):
pass

View File

@ -27,19 +27,24 @@ 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))
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"
@ -52,7 +57,6 @@ def _init(config_path, force, merge, skip_interactive=False):
click.secho("Exiting.")
sys.exit(0)
config = Config()
if not skip_interactive:

View File

@ -2,9 +2,7 @@ import sys
import setuptools
sys.path.append("manim_slides") # To avoid importing manim, which may not be installed
from __version__ import __version__ as version
from manim_slides import __version__ as version
if sys.version_info < (3, 7):
raise RuntimeError("This package requires Python 3.7+")
@ -30,10 +28,11 @@ setuptools.setup(
python_requires=">=3.7",
install_requires=[
"click>=8.0",
"click-default-group>=1.2"
"click-default-group>=1.2",
"numpy>=1.19.3",
"pydantic>=1.9.1",
"opencv-python>=4.6",
"tqdm",
],
classifiers=[
"Programming Language :: Python :: 3",