Compare commits

...

51 Commits

Author SHA1 Message Date
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
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
45e923a3ca bump to v0.1.2 2021-04-24 15:39:57 +02:00
4bbc387b81 disable caching limit while rendering 2021-04-24 15:39:34 +02:00
01882842e0 bump version 2021-04-24 15:19:22 +02:00
574cb9c2f6 correct lag playback compensation 2021-04-24 15:18:11 +02:00
1827699319 Next pause slide should start at the end of last loop 2021-04-24 15:14:57 +02:00
f4ed7b1b00 run deploy pipeline only if tag 2021-04-24 14:31:14 +02:00
3e641fa3dc fix typo 2021-04-22 22:01:01 +02:00
abef878811 Updated readme 2021-04-22 21:59:31 +02:00
19 changed files with 545 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
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
if: startsWith(github.ref, 'refs/tags')
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 }}

4
.gitignore vendored
View File

@ -9,3 +9,7 @@ __pycache__/
/presentation
/.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

103
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
call `self.start_loop()` and `self.stop_loop()` when you want to loop some animations
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,10 +49,16 @@ class Example(Slide):
self.wait()
```
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(..)`.
## Default Keybindings
To start the presentation using `Scene1`, `Scene2` and so on simply run:
```
manim-slides Scene1 Scene2...
```
Default keybindings to control the presentation
## Keybindings
Default keybindings to control the presentation:
| Keybinding | Action |
|:-----------:|:------------------------:|
@ -59,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

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.0.1"

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,44 +34,59 @@ 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.lastframe = []
self.caps = [None for _ in self.files]
self.reset()
self.load_files()
self.add_last_slide()
def reset(self):
self.current_animation = 0
self.current_slide_i = 0
def add_last_slide(self):
last_slide_end = self.slides[-1]["end_animation"]
last_animation = len(self.files) - 1
last_animation = len(self.files)
self.slides.append(dict(
start_animation = last_slide_end,
end_animation = last_animation,
type = "last",
number = len(self.slides) + 1
number = len(self.slides) + 1,
terminated = False
))
def 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):
@ -80,52 +94,84 @@ class Presentation:
@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
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:
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
@ -135,12 +181,13 @@ 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):
@ -186,22 +233,20 @@ class Display:
def handle_key(self):
sleep_time = math.ceil(1000/self.current_presentation.fps)
lag = now() - self.last_time
self.last_time = now()
key = cv2.waitKey(fix_time(sleep_time - 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()
@ -209,8 +254,8 @@ 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
@ -219,26 +264,29 @@ class Display:
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
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
@ -28,7 +32,7 @@ class Slide(Scene):
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):
@ -41,9 +45,17 @@ 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)
@ -72,3 +84,6 @@ class Slide(Scene):
files=files
), f)
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,47 @@
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
sys.path.append("manim_slides") # To avoid importing manim, which may not be installed
from __version__ 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.1.0",
author="Federico A. Galatolo",
author_email="federico.galatolo@ing.unipi.it",
description="",
url="",
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/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