Compare commits

..

14 Commits

20 changed files with 442 additions and 220 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>.

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,17 +49,16 @@ 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...
```
## Keybindings
Default keybindings to control the presentation
Default keybindings to control the presentation:
| Keybinding | Action |
|:-----------:|:------------------------:|
@ -67,51 +69,69 @@ Default keybindings to control the presentation
| Q | Quit |
You can run the **configuration wizard** with
You can run the **configuration wizard** with:
```
manim-presentation-wizard
manim-slides wizard
```
Alternatively 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
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 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,7 @@
from manim import *
from manim_presentation import Slide
from manim_slides import Slide, ThreeDSlide
class Example(Slide):
def construct(self):
@ -16,13 +18,35 @@ 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()
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

View File

@ -1,51 +0,0 @@
import cv2
import numpy as np
import json
import os
import sys
def prompt(question):
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
display = np.zeros((130, 420), np.uint8)
cv2.putText(
display,
"* Manim Presentation Wizard *",
(50, 33),
*font_args
)
cv2.putText(
display,
question,
(30, 85),
*font_args
)
cv2.imshow("wizard", display)
return cv2.waitKeyEx(-1)
def main():
if(os.path.exists("./manim-presentation.json")):
print("The manim-presentation.json configuration file exists")
ans = input("Do you want to continue and overwrite it? (y/n): ")
if ans != "y": sys.exit(0)
prompt("Press any key to continue")
PLAYPAUSE_KEY = prompt("Press the PLAY/PAUSE key")
CONTINUE_KEY = prompt("Press the CONTINUE/NEXT key")
BACK_KEY = prompt("Press the BACK key")
REWIND_KEY = prompt("Press the REWIND key")
QUIT_KEY = prompt("Press the QUIT key")
config_file = open("./manim-presentation.json", "w")
json.dump(dict(
PLAYPAUSE_KEY=PLAYPAUSE_KEY,
CONTINUE_KEY=CONTINUE_KEY,
BACK_KEY=BACK_KEY,
REWIND_KEY=REWIND_KEY,
QUIT_KEY=QUIT_KEY
), config_file)
config_file.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.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,34 +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
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)
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
@ -50,7 +34,7 @@ def fix_time(x):
return x if x > 0 else 1
class Presentation:
def __init__(self, config, last_frame_next=False):
def __init__(self, config, last_frame_next: bool = False):
self.last_frame_next = last_frame_next
self.slides = config["slides"]
self.files = config["files"]
@ -58,7 +42,7 @@ class Presentation:
self.lastframe = []
self.caps = [None for _ in self.files]
self.reset()
self.reset()
self.add_last_slide()
def add_last_slide(self):
@ -78,14 +62,14 @@ class Presentation:
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()
@ -93,7 +77,7 @@ class Presentation:
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
@ -107,7 +91,7 @@ class Presentation:
@property
def current_slide(self):
return self.slides[self.current_slide_i]
@property
def current_cap(self):
self.load_this_cap(self.current_animation)
@ -159,14 +143,15 @@ class Presentation:
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, fullscreen=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
@ -178,11 +163,11 @@ class Display:
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)
@ -199,11 +184,11 @@ class Display:
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)
cv2.imshow("Video", self.lastframe)
def show_info(self):
info = np.zeros((130, 420), np.uint8)
@ -243,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.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()
@ -269,40 +254,39 @@ class Display:
else:
self.current_presentation.prev()
self.state = State.PLAYING
elif key == Config.REWIND_KEY:
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")
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()
@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, last_frame_next=args.last_frame_next))
presentations.append(Presentation(config, last_frame_next=last_frame_next))
display = Display(presentations, start_paused=args.start_paused, fullscreen=args.fullscreen)
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,13 +1,16 @@
import os
import json
import os
import shutil
from manim import Scene, config
from manim.animation.animation import Wait
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
@ -17,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",
@ -27,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(
@ -43,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))
@ -81,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,32 +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.2.1",
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 = {
entry_points={
"console_scripts": [
"manim_presentation=manim_presentation.present:main",
"manim-presentation=manim_presentation.present:main",
"manim-presentation-wizard=manim_presentation.wizard:main",
"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