Compare commits

...

52 Commits
v2.0 ... v3.2.0

Author SHA1 Message Date
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
b3210ec285 fix: version bug & trick 2022-07-13 17:08:06 +02:00
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
22 changed files with 1071 additions and 464 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 }}

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

@ -0,0 +1,55 @@
# 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]
steps:
- uses: actions/checkout@v2
# 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
- 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 }}

10
.gitignore vendored
View File

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

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

@ -0,0 +1,17 @@
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/psf/black
rev: 22.6.0
hooks:
- id: black
- 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>.

110
README.md
View File

@ -1,32 +1,36 @@
# 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-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 extends to work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
## 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
# or: from manimlib import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
@ -46,66 +50,94 @@ class Example(Slide):
self.wait()
```
You **must** end your `Slide` with a `self.play(...)` or a `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 |
|:-----------:|:------------------------:|
| Right Arrow | Continue/Next Slide |
| Left Arrow | Previous Slide |
| R | Re-Animate Current Slide |
| V | Reverse Current Slide |
| 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
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` `REVERSE` `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
# or
pip install manimgl manim-slides
```
Install `manim` and `manim_presentation`
Render the example scene:
```
pip install manim manim-presentation opencv-python
```
Render the example scene
```
manim -qh example.py
manim -qh example.py Example
# or
manimgl --hd example.py Example
```
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
- [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
- [x] 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 and 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,9 @@
from manim import *
from manim_presentation import Slide
# If you want to use manimgl, uncomment change
# manim to manimlib
from manimlib import *
from manim_slides import Slide, ThreeDSlide
class Example(Slide):
def construct(self):
@ -16,13 +20,48 @@ class Example(Slide):
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.play(dot.animate.move_to(RIGHT*3))
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(radius=3, color=BLUE)
dot = Dot(color=RED)
self.add(axes)
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
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.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))

View File

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

View File

@ -1,299 +0,0 @@
import cv2
import numpy as np
import os
import sys
import json
import math
import time
import argparse
from enum import Enum
import platform
class Config:
@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
PAUSED = 1
WAIT = 2
END = 3
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 "..."
def now():
return round(time.time() * 1000)
def fix_time(x):
return x if x > 0 else 1
class Presentation:
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 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
))
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):
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.rewind_slide()
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):
return self.slides[self.current_slide_i]
@property
def current_cap(self):
return self.caps[self.current_animation]
@property
def fps(self):
return self.current_cap.get(cv2.CAP_PROP_FPS)
# 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
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":
self.current_slide["terminated"] = True
elif self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation:
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, fullscreen=False):
self.presentations = presentations
self.start_paused = start_paused
self.state = State.PLAYING
self.lastframe = None
self.current_presentation_i = 0
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:
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
if self.state == State.END:
if self.current_presentation_i == len(self.presentations) - 1:
self.quit()
else:
self.current_presentation_i += 1
self.state = State.PLAYING
self.handle_key()
self.show_video()
self.show_info()
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)
grid_x = [30, 230]
grid_y = [30, 70, 110]
cv2.putText(
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
)
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
(grid_x[0], grid_y[1]),
*font_args
)
cv2.putText(
info,
f"Slide Type: {self.current_presentation.current_slide['type']}",
(grid_x[1], grid_y[1]),
*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
)
cv2.imshow("Info", info)
def handle_key(self):
sleep_time = math.ceil(1000/self.current_presentation.fps)
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
if key == Config.QUIT_KEY:
self.quit()
elif self.state == State.PLAYING and key == Config.PLAYPAUSE_KEY:
self.state = State.PAUSED
elif self.state == State.PAUSED and key == Config.PLAYPAUSE_KEY:
self.state = State.PLAYING
elif self.state == State.WAIT and (key == Config.CONTINUE_KEY or key == Config.PLAYPAUSE_KEY):
self.current_presentation.next()
self.state = State.PLAYING
elif self.state == State.PLAYING and key == Config.CONTINUE_KEY:
self.current_presentation.next()
elif key == Config.BACK_KEY:
if self.current_presentation.current_slide_i == 0:
self.current_presentation_i = max(0, self.current_presentation_i - 1)
self.current_presentation.reset()
self.state = State.PLAYING
else:
self.current_presentation.prev()
self.state = State.PLAYING
elif key == Config.REWIND_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")
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:
config_file = os.path.join(args.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, last_frame_next=args.last_frame_next))
display = Display(presentations, start_paused=args.start_paused, fullscreen=args.fullscreen)
display.run()
if __name__ == "__main__":
main()

View File

@ -1,83 +0,0 @@
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):
self.output_folder = kwargs.pop("output_folder", "./presentation")
super(Slide, self).__init__(*args, **kwargs)
self.slides = list()
self.current_slide = 1
self.current_animation = 0
self.loop_start_animation = None
self.pause_start_animation = 0
def play(self, *args, **kwargs):
super(Slide, self).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.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"
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
))
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):
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))
shutil.copyfile(src_file, dst_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()

2
manim_slides/__init__.py Normal file
View File

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

View File

@ -0,0 +1 @@
__version__ = "3.2.0"

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

49
manim_slides/config.py Normal file
View File

@ -0,0 +1,49 @@
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")
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")
@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(
"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

22
manim_slides/main.py Normal file
View File

@ -0,0 +1,22 @@
import click
from click_default_group import DefaultGroup
from . import __version__
from .present import list_scenes, 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(list_scenes)
cli.add_command(present)
cli.add_command(wizard)
cli.add_command(init)
if __name__ == "__main__":
cli()

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

416
manim_slides/present.py Normal file
View File

@ -0,0 +1,416 @@
import json
import math
import os
import platform
import sys
import time
from enum import IntEnum, auto, unique
if platform.system() == "Windows":
import ctypes
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
from .slide import reverse_video_path
@unique
class State(IntEnum):
PLAYING = auto()
PAUSED = auto()
WAIT = auto()
END = auto()
def __str__(self):
return self.name.capitalize()
def now() -> int:
return round(time.time() * 1000)
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 = [path for path in config["files"]]
self.reverse = False
self.reversed_slide = -1
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)
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
self.load_this_cap(0)
self.current_slide_i = 0
self.slides[-1]["terminated"] = False
def next(self):
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.rewind_slide()
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: 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 not self.caps[i] is None:
self.caps[i].release()
self.caps[i] = None
# load this cap
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):
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)
# 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
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":
self.current_slide["terminated"] = True
elif (
self.current_slide["type"] == "last"
and self.current_slide["end_animation"] == self.current_animation
):
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, config, start_paused=False, fullscreen=False):
self.presentations = presentations
self.start_paused = start_paused
self.config = config
self.state = State.PLAYING
self.lastframe = None
self.current_presentation_i = 0
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
)
def resize_frame_to_screen(self, frame: np.ndarray):
frame_height, frame_width = frame.shape
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):
return self.presentations[self.current_presentation_i]
def run(self):
while True:
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
if self.state == State.END:
if self.current_presentation_i == len(self.presentations) - 1:
self.quit()
else:
self.current_presentation_i += 1
self.state = State.PLAYING
self.handle_key()
self.show_video()
self.show_info()
def show_video(self):
self.lag = now() - self.last_time
self.last_time = now()
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)
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
grid_x = [30, 230]
grid_y = [30, 70, 110]
cv2.putText(
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)
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
(grid_x[0], grid_y[1]),
*font_args,
)
cv2.putText(
info,
f"Slide Type: {self.current_presentation.current_slide['type']}",
(grid_x[1], grid_y[1]),
*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,
)
cv2.imshow("Info", info)
def handle_key(self):
sleep_time = math.ceil(1000 / self.current_presentation.fps)
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
if self.config.QUIT.match(key):
self.quit()
elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key):
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)
):
self.current_presentation.next()
self.state = State.PLAYING
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
self.current_presentation.next()
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()
self.state = State.PLAYING
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("--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."""
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 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))
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()

194
manim_slides/slide.py Normal file
View File

@ -0,0 +1,194 @@
import json
import os
import platform
import shutil
import subprocess
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):
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
self.current_animation = 0
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().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.current_slide += 1
self.pause_start_animation = self.current_animation
def start_loop(self):
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(
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 save_slides(self, use_cache=True):
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)
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 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)
logger.info(
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
)
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

75
manim_slides/wizard.py Normal file
View File

@ -0,0 +1,75 @@
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,46 @@
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
from manim_slides import __version__ as version
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.2.0",
author="Federico A. Galatolo",
author_email="federico.galatolo@ing.unipi.it",
name="manim-slides",
version=version,
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",
"manim",
"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