mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 11:05:54 +08:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
db5fa33e0c | |||
da0972ef43 | |||
8081617e29 | |||
9247568531 | |||
94e399ba91 | |||
b3f988421a | |||
b9eb2b395a | |||
f3d2c4e731 | |||
ddeb20646d | |||
588c4b285f | |||
6fdf6d670b | |||
2a8b6734ec | |||
d2f04341fa | |||
19fa17212c | |||
73107f5065 | |||
0429cf3543 | |||
87bbc9a8c6 | |||
c0a5a70f93 | |||
ea171e1301 | |||
d1c3e9c075 | |||
38e03db9e9 | |||
2eafa0b82e | |||
8a4fea687d | |||
128d6718ae | |||
22cbb7ec94 | |||
5feb13da10 | |||
90b2e4d46b | |||
43b9fa7cf7 | |||
1cc070db86 | |||
c5274fb57f | |||
2a136ed585 | |||
c9390f0e59 | |||
41de205675 | |||
133ec17ebb | |||
09199777e0 |
22
.github/workflows/deploy-pipy.yaml
vendored
22
.github/workflows/deploy-pipy.yaml
vendored
@ -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
49
.github/workflows/python-publish.yml
vendored
Normal 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
6
.gitignore
vendored
@ -8,4 +8,8 @@ __pycache__/
|
|||||||
/media
|
/media
|
||||||
/presentation
|
/presentation
|
||||||
|
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|
||||||
|
slides/
|
||||||
|
|
||||||
|
.manim-slides.json
|
||||||
|
13
.pre-commit-config.yaml
Normal file
13
.pre-commit-config.yaml
Normal 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"]
|
@ -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
|
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
|
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
|
Public License instead of this License. But first, please read
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
102
README.md
102
README.md
@ -1,32 +1,35 @@
|
|||||||
# manim-presentation
|
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
||||||
|
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
||||||
|

|
||||||
|
# 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
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install manim-presentation opencv-python
|
pip install manim-slides
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Use the class `Slide` as your scenes base class
|
Use the class `Slide` as your scenes base class:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from manim_presentation import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
class Example(Slide):
|
class Example(Slide):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings)
|
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)
|
|
||||||
|
|
||||||
|
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
|
||||||
```python
|
```python
|
||||||
from manim import *
|
from manim import *
|
||||||
from manim_presentation import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
class Example(Slide):
|
class Example(Slide):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
@ -46,15 +49,16 @@ class Example(Slide):
|
|||||||
self.wait()
|
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:
|
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 |
|
| Keybinding | Action |
|
||||||
|:-----------:|:------------------------:|
|
|:-----------:|:------------------------:|
|
||||||
@ -64,42 +68,70 @@ Default keybindings to control the presentation
|
|||||||
| Spacebar | Play/Pause |
|
| Spacebar | Play/Pause |
|
||||||
| Q | Quit |
|
| 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
|
## Run Example
|
||||||
|
|
||||||
Clone this repository
|
Clone this repository:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/galatolofederico/manim-presentation.git
|
git clone https://github.com/jeertmans/manim-slides.git
|
||||||
cd manim-presentation
|
cd manim-slides
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a virtualenv
|
Install `manim` and `manim-slides`:
|
||||||
|
|
||||||
```
|
```
|
||||||
virtualenv --python=python3.7 env
|
pip install manim manim-slides
|
||||||
. ./env/bin/activate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the presentation
|
Run the presentation
|
||||||
|
```
|
||||||
|
manim-slides Example
|
||||||
|
```
|
||||||
|
|
||||||
```
|
Below is a small recording of me playing with the slides back and forth.
|
||||||
manim_presentation Example
|
|
||||||
```
|

|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
## 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
|
||||||
|
35
example.py
35
example.py
@ -1,5 +1,7 @@
|
|||||||
from manim import *
|
from manim import *
|
||||||
from manim_presentation import Slide
|
|
||||||
|
from manim_slides import Slide, ThreeDSlide
|
||||||
|
|
||||||
|
|
||||||
class Example(Slide):
|
class Example(Slide):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
@ -16,4 +18,35 @@ class Example(Slide):
|
|||||||
self.play(dot.animate.move_to(ORIGIN))
|
self.play(dot.animate.move_to(ORIGIN))
|
||||||
self.pause()
|
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.wait()
|
||||||
|
|
||||||
|
self.start_loop()
|
||||||
|
self.play(circle.animate.move_to(ORIGIN + UP))
|
||||||
|
self.end_loop()
|
||||||
|
|
||||||
|
self.play(circle.animate.move_to(ORIGIN))
|
||||||
|
|
||||||
|
# TODO: fixit
|
||||||
|
@ -1 +0,0 @@
|
|||||||
from manim_presentation.slide import Slide
|
|
1
manim_slides/__init__.py
Normal file
1
manim_slides/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .slide import Slide, ThreeDSlide
|
28
manim_slides/commons.py
Normal file
28
manim_slides/commons.py
Normal 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
48
manim_slides/config.py
Normal 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
11
manim_slides/defaults.py
Normal 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
20
manim_slides/main.py
Normal 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()
|
@ -1,19 +1,18 @@
|
|||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import argparse
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
class Config:
|
import click
|
||||||
QUIT_KEY = ord("q")
|
import cv2
|
||||||
CONTINUE_KEY = 83 #right arrow
|
import numpy as np
|
||||||
BACK_KEY = 81 #left arrow
|
|
||||||
REWIND_KEY = ord("r")
|
from .commons import config_path_option
|
||||||
PLAYPAUSE_KEY = 32 #spacebar
|
from .config import Config
|
||||||
|
from .defaults import CONFIG_PATH, FOLDER_PATH
|
||||||
|
|
||||||
|
|
||||||
class State(Enum):
|
class State(Enum):
|
||||||
PLAYING = 0
|
PLAYING = 0
|
||||||
@ -35,83 +34,124 @@ def fix_time(x):
|
|||||||
return x if x > 0 else 1
|
return x if x > 0 else 1
|
||||||
|
|
||||||
class Presentation:
|
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.slides = config["slides"]
|
||||||
self.files = config["files"]
|
self.files = config["files"]
|
||||||
|
|
||||||
self.reset()
|
self.lastframe = []
|
||||||
self.load_files()
|
|
||||||
self.add_last_slide()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.current_animation = 0
|
|
||||||
self.current_slide_i = 0
|
|
||||||
|
|
||||||
|
self.caps = [None for _ in self.files]
|
||||||
|
self.reset()
|
||||||
|
self.add_last_slide()
|
||||||
|
|
||||||
def add_last_slide(self):
|
def add_last_slide(self):
|
||||||
last_slide_end = self.slides[-1]["end_animation"]
|
last_slide_end = self.slides[-1]["end_animation"]
|
||||||
last_animation = len(self.files) - 1
|
last_animation = len(self.files)
|
||||||
self.slides.append(dict(
|
self.slides.append(dict(
|
||||||
start_animation = last_slide_end,
|
start_animation = last_slide_end,
|
||||||
end_animation = last_animation,
|
end_animation = last_animation,
|
||||||
type = "last",
|
type = "last",
|
||||||
number = len(self.slides) + 1
|
number = len(self.slides) + 1,
|
||||||
|
terminated = False
|
||||||
))
|
))
|
||||||
|
|
||||||
def load_files(self):
|
|
||||||
self.caps = list()
|
def reset(self):
|
||||||
for f in self.files:
|
self.current_animation = 0
|
||||||
self.caps.append(cv2.VideoCapture(f))
|
self.load_this_cap(0)
|
||||||
|
self.current_slide_i = 0
|
||||||
|
self.slides[-1]["terminated"] = False
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1)
|
if self.current_slide["type"] == "last":
|
||||||
self.current_animation = self.current_slide["start_animation"]
|
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):
|
def prev(self):
|
||||||
self.current_slide_i = max(0, self.current_slide_i - 1)
|
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_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
|
@property
|
||||||
def current_slide(self):
|
def current_slide(self):
|
||||||
return self.slides[self.current_slide_i]
|
return self.slides[self.current_slide_i]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_cap(self):
|
def current_cap(self):
|
||||||
|
self.load_this_cap(self.current_animation)
|
||||||
return self.caps[self.current_animation]
|
return self.caps[self.current_animation]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fps(self):
|
def fps(self):
|
||||||
return self.current_cap.get(cv2.CAP_PROP_FPS)
|
return self.current_cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
|
||||||
def get_frame_and_state(self):
|
# This function updates the state given the previous state.
|
||||||
ret, frame = self.current_cap.read()
|
# It does this by reading the video information and checking if the state is still correct.
|
||||||
state = State.PLAYING
|
# It returns the frame to show (lastframe) and the new state.
|
||||||
if ret:
|
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
|
self.lastframe = frame
|
||||||
else:
|
elif state in [state.WAIT, state.PAUSED]:
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
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["end_animation"] == self.current_animation + 1:
|
||||||
if self.current_slide["type"] == "slide":
|
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
|
state = State.WAIT
|
||||||
elif self.current_slide["type"] == "loop":
|
elif self.current_slide["type"] == "loop":
|
||||||
self.current_animation = self.current_slide["start_animation"]
|
self.current_animation = self.current_slide["start_animation"]
|
||||||
|
state = State.PLAYING
|
||||||
|
self.rewind_slide()
|
||||||
elif self.current_slide["type"] == "last":
|
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:
|
elif self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation:
|
||||||
return self.lastframe, State.END
|
state = State.WAIT
|
||||||
else:
|
else:
|
||||||
|
# Play next video!
|
||||||
self.current_animation += 1
|
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
|
return self.lastframe, state
|
||||||
|
|
||||||
|
|
||||||
class Display:
|
class Display:
|
||||||
def __init__(self, presentations, start_paused=False):
|
def __init__(self, presentations, config, start_paused=False, fullscreen=False):
|
||||||
self.presentations = presentations
|
self.presentations = presentations
|
||||||
self.start_paused = start_paused
|
self.start_paused = start_paused
|
||||||
|
self.config = config
|
||||||
|
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
self.lastframe = None
|
self.lastframe = None
|
||||||
@ -119,15 +159,19 @@ class Display:
|
|||||||
|
|
||||||
self.lag = 0
|
self.lag = 0
|
||||||
self.last_time = now()
|
self.last_time = now()
|
||||||
|
|
||||||
|
if fullscreen:
|
||||||
|
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
|
||||||
|
cv2.setWindowProperty("Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_presentation(self):
|
def current_presentation(self):
|
||||||
return self.presentations[self.current_presentation_i]
|
return self.presentations[self.current_presentation_i]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while True:
|
||||||
if self.state == State.PLAYING:
|
self.lastframe, self.state = self.current_presentation.update_state(self.state)
|
||||||
self.lastframe, self.state = self.current_presentation.get_frame_and_state()
|
if self.state == State.PLAYING or self.state == State.PAUSED:
|
||||||
if self.start_paused:
|
if self.start_paused:
|
||||||
self.state = State.PAUSED
|
self.state = State.PAUSED
|
||||||
self.start_paused = False
|
self.start_paused = False
|
||||||
@ -137,16 +181,15 @@ class Display:
|
|||||||
else:
|
else:
|
||||||
self.current_presentation_i += 1
|
self.current_presentation_i += 1
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
|
self.handle_key()
|
||||||
self.show_video()
|
self.show_video()
|
||||||
self.show_info()
|
self.show_info()
|
||||||
self.handle_key()
|
|
||||||
|
|
||||||
def show_video(self):
|
def show_video(self):
|
||||||
self.lag = now() - self.last_time
|
self.lag = now() - self.last_time
|
||||||
self.last_time = now()
|
self.last_time = now()
|
||||||
cv2.imshow("Video", self.lastframe)
|
cv2.imshow("Video", self.lastframe)
|
||||||
|
|
||||||
def show_info(self):
|
def show_info(self):
|
||||||
info = np.zeros((130, 420), np.uint8)
|
info = np.zeros((130, 420), np.uint8)
|
||||||
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
||||||
@ -185,25 +228,25 @@ class Display:
|
|||||||
((grid_x[0]+grid_x[1])//2, grid_y[2]),
|
((grid_x[0]+grid_x[1])//2, grid_y[2]),
|
||||||
*font_args
|
*font_args
|
||||||
)
|
)
|
||||||
|
|
||||||
cv2.imshow("Info", info)
|
cv2.imshow("Info", info)
|
||||||
|
|
||||||
def handle_key(self):
|
def handle_key(self):
|
||||||
sleep_time = math.ceil(1000/self.current_presentation.fps)
|
sleep_time = math.ceil(1000/self.current_presentation.fps)
|
||||||
key = cv2.waitKey(fix_time(sleep_time - self.lag)) & 0xFF
|
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
|
||||||
|
|
||||||
if key == Config.QUIT_KEY:
|
if self.config.QUIT.match(key):
|
||||||
self.quit()
|
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
|
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
|
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.current_presentation.next()
|
||||||
self.state = State.PLAYING
|
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()
|
self.current_presentation.next()
|
||||||
elif key == Config.BACK_KEY:
|
elif self.config.BACK.match(key):
|
||||||
if self.current_presentation.current_slide_i == 0:
|
if self.current_presentation.current_slide_i == 0:
|
||||||
self.current_presentation_i = max(0, self.current_presentation_i - 1)
|
self.current_presentation_i = max(0, self.current_presentation_i - 1)
|
||||||
self.current_presentation.reset()
|
self.current_presentation.reset()
|
||||||
@ -211,36 +254,39 @@ class Display:
|
|||||||
else:
|
else:
|
||||||
self.current_presentation.prev()
|
self.current_presentation.prev()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
elif key == Config.REWIND_KEY:
|
elif self.config.REWIND.match(key):
|
||||||
self.current_presentation.rewind()
|
self.current_presentation.rewind_slide()
|
||||||
self.state = State.PLAYING
|
self.state = State.PLAYING
|
||||||
|
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
@click.command()
|
||||||
parser = argparse.ArgumentParser()
|
@click.argument("scenes", nargs=-1)
|
||||||
|
@config_path_option
|
||||||
parser.add_argument("scenes", metavar="scenes", type=str, nargs="+", help="Scenes to present")
|
@click.option("--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.")
|
||||||
parser.add_argument("--folder", type=str, default="./presentation", help="Presentation files folder")
|
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||||
parser.add_argument("--start-paused", action="store_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).")
|
||||||
args = parser.parse_args()
|
@click.help_option("-h", "--help")
|
||||||
args.folder = os.path.normcase(args.folder)
|
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
|
||||||
|
"""Present the different scenes"""
|
||||||
|
|
||||||
presentations = list()
|
presentations = list()
|
||||||
for scene in args.scenes:
|
for scene in scenes:
|
||||||
config_file = os.path.join(args.folder, f"{scene}.json")
|
config_file = os.path.join(folder, f"{scene}.json")
|
||||||
if not os.path.exists(config_file):
|
if not os.path.exists(config_file):
|
||||||
raise Exception(f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class")
|
raise 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))
|
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()
|
display.run()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,12 +1,16 @@
|
|||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from manim import Scene, config
|
|
||||||
|
from manim import Scene, ThreeDScene, config
|
||||||
|
|
||||||
|
from .defaults import FOLDER_PATH
|
||||||
|
|
||||||
|
|
||||||
class Slide(Scene):
|
class Slide(Scene):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
|
||||||
self.output_folder = kwargs.pop("output_folder", "./presentation")
|
|
||||||
super(Slide, self).__init__(*args, **kwargs)
|
super(Slide, self).__init__(*args, **kwargs)
|
||||||
|
self.output_folder = output_folder
|
||||||
self.slides = list()
|
self.slides = list()
|
||||||
self.current_slide = 1
|
self.current_slide = 1
|
||||||
self.current_animation = 0
|
self.current_animation = 0
|
||||||
@ -16,7 +20,7 @@ class Slide(Scene):
|
|||||||
def play(self, *args, **kwargs):
|
def play(self, *args, **kwargs):
|
||||||
super(Slide, self).play(*args, **kwargs)
|
super(Slide, self).play(*args, **kwargs)
|
||||||
self.current_animation += 1
|
self.current_animation += 1
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
self.slides.append(dict(
|
self.slides.append(dict(
|
||||||
type="slide",
|
type="slide",
|
||||||
@ -26,11 +30,11 @@ class Slide(Scene):
|
|||||||
))
|
))
|
||||||
self.current_slide += 1
|
self.current_slide += 1
|
||||||
self.pause_start_animation = self.current_animation
|
self.pause_start_animation = self.current_animation
|
||||||
|
|
||||||
def start_loop(self):
|
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
|
self.loop_start_animation = self.current_animation
|
||||||
|
|
||||||
def end_loop(self):
|
def end_loop(self):
|
||||||
assert self.loop_start_animation is not None, "You have to start a loop before ending it"
|
assert self.loop_start_animation is not None, "You have to start a loop before ending it"
|
||||||
self.slides.append(dict(
|
self.slides.append(dict(
|
||||||
@ -42,32 +46,32 @@ class Slide(Scene):
|
|||||||
self.current_slide += 1
|
self.current_slide += 1
|
||||||
self.loop_start_animation = None
|
self.loop_start_animation = None
|
||||||
self.pause_start_animation = self.current_animation
|
self.pause_start_animation = self.current_animation
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
def render(self, *args, **kwargs):
|
||||||
# We need to disable the caching limit since we rely on intermidiate files
|
# We need to disable the caching limit since we rely on intermidiate files
|
||||||
max_files_cached = config["max_files_cached"]
|
max_files_cached = config["max_files_cached"]
|
||||||
config["max_files_cached"] = float("inf")
|
config["max_files_cached"] = float("inf")
|
||||||
|
|
||||||
super(Slide, self).render(*args, **kwargs)
|
super(Slide, self).render(*args, **kwargs)
|
||||||
|
|
||||||
config["max_files_cached"] = max_files_cached
|
config["max_files_cached"] = max_files_cached
|
||||||
|
|
||||||
if not os.path.exists(self.output_folder):
|
if not os.path.exists(self.output_folder):
|
||||||
os.mkdir(self.output_folder)
|
os.mkdir(self.output_folder)
|
||||||
|
|
||||||
files_folder = os.path.join(self.output_folder, "files")
|
files_folder = os.path.join(self.output_folder, "files")
|
||||||
if not os.path.exists(files_folder):
|
if not os.path.exists(files_folder):
|
||||||
os.mkdir(files_folder)
|
os.mkdir(files_folder)
|
||||||
|
|
||||||
scene_name = type(self).__name__
|
scene_name = type(self).__name__
|
||||||
scene_files_folder = os.path.join(files_folder, scene_name)
|
scene_files_folder = os.path.join(files_folder, scene_name)
|
||||||
|
|
||||||
if os.path.exists(scene_files_folder):
|
if os.path.exists(scene_files_folder):
|
||||||
shutil.rmtree(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)
|
os.mkdir(scene_files_folder)
|
||||||
|
|
||||||
files = list()
|
files = list()
|
||||||
for src_file in self.renderer.file_writer.partial_movie_files:
|
for src_file in self.renderer.file_writer.partial_movie_files:
|
||||||
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file))
|
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file))
|
||||||
@ -79,4 +83,7 @@ class Slide(Scene):
|
|||||||
slides=self.slides,
|
slides=self.slides,
|
||||||
files=files
|
files=files
|
||||||
), f)
|
), f)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
class ThreeDSlide(ThreeDScene, Slide):
|
||||||
|
pass
|
71
manim_slides/wizard.py
Normal file
71
manim_slides/wizard.py
Normal 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}`")
|
41
setup.py
41
setup.py
@ -1,28 +1,43 @@
|
|||||||
import setuptools
|
import sys
|
||||||
import os
|
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "README.md"), "r") as fh:
|
import setuptools
|
||||||
long_description = fh.read()
|
|
||||||
|
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(
|
setuptools.setup(
|
||||||
name="manim_presentation",
|
name="manim-slides",
|
||||||
version="0.1.5",
|
version="3.0.0",
|
||||||
author="Federico A. Galatolo",
|
author="Jérome Eertmans (previously, Federico A. Galatolo)",
|
||||||
author_email="federico.galatolo@ing.unipi.it",
|
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
|
||||||
description="Tool for live presentations using manim",
|
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=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
packages=setuptools.find_packages(),
|
packages=setuptools.find_packages(),
|
||||||
entry_points = {
|
entry_points={
|
||||||
"console_scripts": ["manim_presentation=manim_presentation.present:main"],
|
"console_scripts": [
|
||||||
|
"manim-slides=manim_slides.main:cli",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
python_requires=">=3.7",
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
"click>=8.0",
|
||||||
|
"click-default-group>=1.2"
|
||||||
|
"numpy>=1.19.3",
|
||||||
|
"pydantic>=1.9.1",
|
||||||
|
"opencv-python>=4.6",
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.7",
|
"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)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Development Status :: 4 - Beta"
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
BIN
static/example.gif
Normal file
BIN
static/example.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 670 KiB |
Reference in New Issue
Block a user