Compare commits

...

42 Commits

Author SHA1 Message Date
db5fa33e0c fix: not including version in package :') 2022-07-12 17:32:13 +02:00
da0972ef43 Running pre-commit 2022-07-12 17:28:14 +02:00
8081617e29 Trick to not import manim 2022-07-12 17:25:28 +02:00
9247568531 Add badges and capitalize name 2022-07-12 17:16:11 +02:00
94e399ba91 Some files to ignore 2022-07-12 17:12:30 +02:00
b3f988421a Renaming LICENSE file 2022-07-12 17:12:13 +02:00
b9eb2b395a (WIP): Add 3D Scene example 2022-07-12 17:11:48 +02:00
f3d2c4e731 Refactor old work 2022-07-12 17:11:32 +02:00
ddeb20646d Update setup file with personal info & deps 2022-07-12 17:10:54 +02:00
588c4b285f Update README 2022-07-12 17:10:38 +02:00
6fdf6d670b Add pre-commit CI 2022-07-12 17:10:06 +02:00
2a8b6734ec Refactor publishing CI 2022-07-12 17:09:55 +02:00
d2f04341fa Add gif of example 2022-07-12 17:09:26 +02:00
19fa17212c Bump version to 0.2.1 2021-06-28 20:20:08 +02:00
73107f5065 Updated readme 2021-06-28 20:19:36 +02:00
0429cf3543 Added manim-presentation-wizard and manim-presentation commands 2021-06-28 20:17:23 +02:00
87bbc9a8c6 Added configuration wizard 2021-06-28 20:16:46 +02:00
c0a5a70f93 Merge branch 'glatteis-main' 2021-06-28 19:47:35 +02:00
ea171e1301 Fix memory problems 2021-06-27 13:53:06 +02:00
d1c3e9c075 major improvements, bumping to 2.0 2021-06-18 21:43:08 +02:00
38e03db9e9 updated readme 2021-06-18 21:33:11 +02:00
2eafa0b82e Solves #4 2021-06-18 21:31:10 +02:00
8a4fea687d added show next frame as last frame hack (#7) 2021-06-18 21:26:46 +02:00
128d6718ae automatically got to next Scene 2021-06-18 21:19:16 +02:00
22cbb7ec94 Merge branch 'glatteis-main' 2021-06-18 20:10:12 +02:00
5feb13da10 updated readme 2021-06-18 20:08:50 +02:00
90b2e4d46b modified example 2021-06-18 20:06:31 +02:00
43b9fa7cf7 removed leftover self.play 2021-06-18 20:05:27 +02:00
1cc070db86 Re added add_last_slide becouse it was broken if the last slide was a loop 2021-06-18 20:02:24 +02:00
c5274fb57f pause() displays final frame not pre-final 2021-06-18 14:43:06 +02:00
2a136ed585 Fix pause, loops 2021-06-11 09:10:02 +02:00
c9390f0e59 Fix loops 2021-06-11 08:42:56 +02:00
41de205675 Add fullscreen option 2021-06-05 14:25:29 +02:00
133ec17ebb Fix a lot of stuff in present.py 2021-06-05 14:16:17 +02:00
09199777e0 Support for windows keys and config file as in #4 2021-06-05 13:36:54 +02:00
bfcf7db26e bump to 0.1.5 2021-05-22 12:14:54 +02:00
b6522f4756 bump version to 0.1.4 2021-05-22 12:10:05 +02:00
4216299b39 Fix #3 2021-05-22 12:09:07 +02:00
cc8df92596 bump version 2021-04-24 16:18:16 +02:00
ef22141f91 safe current_slide_i increment 2021-04-24 16:16:11 +02:00
6881e24954 fix readme typo 2021-04-24 15:50:23 +02:00
d7d6ac610a readme update 2021-04-24 15:44:38 +02:00
18 changed files with 524 additions and 169 deletions

View File

@ -1,22 +0,0 @@
name: Publish top PyPI
on: push
jobs:
build-n-publish:
name: Build and publish to PyPI
if: startsWith(github.ref, 'refs/tags')
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install
run: python -m pip install build --user
- name: Build binary wheel and a source tarball
run: python -m build --sdist --wheel --outdir dist/ .
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}

49
.github/workflows/python-publish.yml vendored Normal file
View File

@ -0,0 +1,49 @@
# From: https://github.com/pypa/cibuildwheel
name: Upload Python Package
on:
push:
release:
types: [published]
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v2
# Used to host cibuildwheel
- uses: actions/setup-python@v2
- name: Install cibuildwheel
run: python -m pip install -U setuptools wheel pip
- name: Build wheels
run: python setup.py sdist
- uses: actions/upload-artifact@v2
with:
name: dist
path: dist/*.tar.*
release:
name: Release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
runs-on: ubuntu-latest
needs: [ build_wheels ]
steps:
- uses: actions/download-artifact@v2
with:
name: dist
path: dist/
- name: Upload to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

6
.gitignore vendored
View File

@ -8,4 +8,8 @@ __pycache__/
/media
/presentation
/.vscode
/.vscode
slides/
.manim-slides.json

13
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,13 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"]

View File

@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

102
README.md
View File

@ -1,32 +1,35 @@
# manim-presentation
[![Latest Release][pypi-version-badge]][pypi-version-url]
[![Python version][pypi-python-version-badge]][pypi-version-url]
![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides)
# Manim Slides
Tool for live presentations using [manim](https://www.manim.community/)
Tool for live presentations using either [manim](http://3b1b.github.io/manim/) or [manim-community](https://www.manim.community/).
> **_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.
## Install
```
pip install manim_presentation opencv-python
pip install manim-slides
```
## Usage
Use the class `Slide` as your scenes base class
Use the class `Slide` as your scenes base class:
```python
from manim_presentation import Slide
from manim_slides import Slide
class Example(Slide):
def construct(self):
...
```
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings)
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue)
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings).
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 *
from manim_presentation import Slide
from manim_slides import Slide
class Example(Slide):
def construct(self):
@ -46,15 +49,16 @@ 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:
```
manim_presentation Scene1 Scene2...
manim-slides Scene1 Scene2...
```
## Default Keybindings
## Keybindings
Default keybindings to control the presentation
Default keybindings to control the presentation:
| Keybinding | Action |
|:-----------:|:------------------------:|
@ -64,42 +68,70 @@ Default keybindings to control the presentation
| Spacebar | Play/Pause |
| Q | Quit |
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`.
A default file can be created with:
```
manim-slides init
```
> **_NOTE:_** `manim-slides` uses `cv2.waitKeyEx()` to wait for keypresses, and directly registers the key code.
## Run Example
Clone this repository
Clone this repository:
```
git clone https://github.com/galatolofederico/manim-presentation.git
cd manim-presentation
git clone https://github.com/jeertmans/manim-slides.git
cd manim-slides
```
Create a virtualenv
Install `manim` and `manim-slides`:
```
virtualenv --python=python3.7 env
. ./env/bin/activate
pip install manim manim-slides
```
Install `manim` and `manim_presentation`
```
pip install manim manim_presentation opencv-python
```
Render the example scene
Render the example scene:
```
manim -qh example.py
```
Run the presentation
```
manim-slides Example
```
```
manim_presentation Example
```
Below is a small recording of me playing with the slides back and forth.
![](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif)
## Comparison with original `manim-presentation`
Here are a few things that I implemented (or that I'm planning to implement) on top of the original work:
- [x] Allowing multiple keys to control one action (useful when you use a laser pointer)
- [x] More robust config files checking
- [x] Dependencies are installed with the package
- [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)
- [ ] Generate docs online
- [ ] Fix the quality problem on Windows platforms with `fullscreen` flag
## Contributions and license
The code is released as Free Software under the [GNU/GPLv3](https://choosealicense.com/licenses/gpl-3.0/) license. Copying, adapting e republishing it is not only consent but also encouraged.
The code is released as Free Software under the [GNU/GPLv3](https://choosealicense.com/licenses/gpl-3.0/) license. Copying, adapting and republishing it is not only consent but also encouraged.
For any further question feel free to reach me at [federico.galatolo@ing.unipi.it](mailto:federico.galatolo@ing.unipi.it) or on Telegram [@galatolo](https://t.me/galatolo)
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
[pypi-version-url]: https://pypi.org/project/manim-slides/
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/manim-slides

View File

@ -1,5 +1,7 @@
from manim import *
from manim_presentation import Slide
from manim_slides import Slide, ThreeDSlide
class Example(Slide):
def construct(self):
@ -16,4 +18,35 @@ class Example(Slide):
self.play(dot.animate.move_to(ORIGIN))
self.pause()
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))
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle=Circle()
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
self.add(circle,axes)
self.begin_ambient_camera_rotation(rate=0.1)
self.pause()
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.wait()
self.start_loop()
self.play(circle.animate.move_to(ORIGIN + UP))
self.end_loop()
self.play(circle.animate.move_to(ORIGIN))
# TODO: fixit

View File

@ -1 +0,0 @@
from manim_presentation.slide import Slide

1
manim_slides/__init__.py Normal file
View File

@ -0,0 +1 @@
from .slide import Slide, ThreeDSlide

28
manim_slides/commons.py Normal file
View File

@ -0,0 +1,28 @@
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
View 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
View 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
View File

@ -0,0 +1,20 @@
import click
from click_default_group import DefaultGroup
from . import __version__
from .present import present
from .wizard import init, wizard
@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()

View File

@ -1,19 +1,18 @@
import cv2
import numpy as np
import os
import sys
import json
import math
import os
import sys
import time
import argparse
from enum import Enum
class Config:
QUIT_KEY = ord("q")
CONTINUE_KEY = 83 #right arrow
BACK_KEY = 81 #left arrow
REWIND_KEY = ord("r")
PLAYPAUSE_KEY = 32 #spacebar
import click
import cv2
import numpy as np
from .commons import config_path_option
from .config import Config
from .defaults import CONFIG_PATH, FOLDER_PATH
class State(Enum):
PLAYING = 0
@ -35,83 +34,124 @@ def fix_time(x):
return x if x > 0 else 1
class Presentation:
def __init__(self, config):
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.reset()
self.load_files()
self.add_last_slide()
def reset(self):
self.current_animation = 0
self.current_slide_i = 0
self.lastframe = []
self.caps = [None for _ in self.files]
self.reset()
self.add_last_slide()
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 load_files(self):
self.caps = list()
for f in self.files:
self.caps.append(cv2.VideoCapture(f))
def reset(self):
self.current_animation = 0
self.load_this_cap(0)
self.current_slide_i = 0
self.slides[-1]["terminated"] = False
def next(self):
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)
def load_this_cap(self,cap_number):
if self.caps[cap_number] == None:
# unload other caps
for i in range(len(self.caps)):
if self.caps[i] != None:
self.caps[i].release()
self.caps[i] = None
# load this cap
self.caps[cap_number] = cv2.VideoCapture(self.files[cap_number])
@property
def current_slide(self):
return self.slides[self.current_slide_i]
@property
def current_cap(self):
self.load_this_cap(self.current_animation)
return self.caps[self.current_animation]
@property
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.load_this_cap(self.next_cap)
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
self.load_this_cap(self.current_animation)
# 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, config, start_paused=False, fullscreen=False):
self.presentations = presentations
self.start_paused = start_paused
self.config = config
self.state = State.PLAYING
self.lastframe = None
@ -119,15 +159,19 @@ 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,16 +181,15 @@ 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
self.last_time = now()
cv2.imshow("Video", self.lastframe)
def show_info(self):
info = np.zeros((130, 420), np.uint8)
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
@ -185,25 +228,25 @@ class Display:
((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)
key = cv2.waitKey(fix_time(sleep_time - self.lag)) & 0xFF
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
if key == Config.QUIT_KEY:
if self.config.QUIT.match(key):
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
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
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.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()
elif key == Config.BACK_KEY:
elif self.config.BACK.match(key):
if self.current_presentation.current_slide_i == 0:
self.current_presentation_i = max(0, self.current_presentation_i - 1)
self.current_presentation.reset()
@ -211,36 +254,39 @@ class Display:
else:
self.current_presentation.prev()
self.state = State.PLAYING
elif key == Config.REWIND_KEY:
self.current_presentation.rewind()
elif self.config.REWIND.match(key):
self.current_presentation.rewind_slide()
self.state = State.PLAYING
def quit(self):
cv2.destroyAllWindows()
sys.exit()
def main():
parser = argparse.ArgumentParser()
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")
args = parser.parse_args()
@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("--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.help_option("-h", "--help")
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
"""Present the different scenes"""
presentations = list()
for scene in args.scenes:
config_file = os.path.join(args.folder, f"{scene}.json")
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")
config = json.load(open(config_file))
presentations.append(Presentation(config))
presentations.append(Presentation(config, last_frame_next=last_frame_next))
display = Display(presentations, start_paused=args.start_paused)
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()
if __name__ == "__main__":
main()

View File

@ -1,12 +1,16 @@
import os
import json
import os
import shutil
from manim import Scene, config
from manim import Scene, ThreeDScene, config
from .defaults import FOLDER_PATH
class Slide(Scene):
def __init__(self, *args, **kwargs):
self.output_folder = kwargs.pop("output_folder", "./presentation")
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
super(Slide, self).__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides = list()
self.current_slide = 1
self.current_animation = 0
@ -16,7 +20,7 @@ class Slide(Scene):
def play(self, *args, **kwargs):
super(Slide, self).play(*args, **kwargs)
self.current_animation += 1
def pause(self):
self.slides.append(dict(
type="slide",
@ -26,11 +30,11 @@ class Slide(Scene):
))
self.current_slide += 1
self.pause_start_animation = self.current_animation
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
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(
@ -42,32 +46,32 @@ class Slide(Scene):
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
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
files_folder = os.path.join(self.output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
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)
if not os.path.exists(scene_files_folder):
if not os.path.exists(scene_files_folder):
os.mkdir(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))
@ -79,4 +83,7 @@ class Slide(Scene):
slides=self.slides,
files=files
), f)
f.close()
f.close()
class ThreeDSlide(ThreeDScene, Slide):
pass

71
manim_slides/wizard.py Normal file
View File

@ -0,0 +1,71 @@
import os
import sys
import click
import cv2
import numpy as np
from .commons import config_options
from .config import Config
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}`")

View File

@ -1,28 +1,43 @@
import setuptools
import os
import sys
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "README.md"), "r") as fh:
long_description = fh.read()
import setuptools
if sys.version_info < (3, 7):
raise RuntimeError("This package requires Python 3.7+")
with open("README.md", "r") as f:
long_description = f.read()
setuptools.setup(
name="manim_presentation",
version="0.1.2",
author="Federico A. Galatolo",
author_email="federico.galatolo@ing.unipi.it",
name="manim-slides",
version="3.0.0",
author="Jérome Eertmans (previously, Federico A. Galatolo)",
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
description="Tool for live presentations using manim",
url="https://github.com/galatolofederico/manim-presentation",
url="https://github.com/jeertmans/manim-slides",
long_description=long_description,
long_description_content_type="text/markdown",
packages=setuptools.find_packages(),
entry_points = {
"console_scripts": ["manim_presentation=manim_presentation.present:main"],
entry_points={
"console_scripts": [
"manim-slides=manim_slides.main:cli",
],
},
python_requires=">=3.7",
install_requires=[
"click>=8.0",
"click-default-group>=1.2"
"numpy>=1.19.3",
"pydantic>=1.9.1",
"opencv-python>=4.6",
],
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Development Status :: 4 - Beta"
],
)
)

BIN
static/example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB