Compare commits

..

50 Commits

Author SHA1 Message Date
d6ec0d3da9 chore(version): update version 2022-09-26 17:30:10 +02:00
546451e019 fix(lib): add a more meaningful error message (#30)
* fix(lib): add a more meaningful error message

This adds an error message to explain why calling `self.pause()` cannot work if no previous animation was played.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-26 17:29:17 +02:00
2457ca8a05 fix(cli): properly rewind / previous slide after reverse (#28)
* fix(cli): properly rewind / previous slide after reverse

Closes #24

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-22 19:11:26 +02:00
9900b3123e fix(cli): revert breaking change 2022-09-22 18:28:27 +02:00
ee92e0aa88 chore(docs): improve CLI docs (#27)
* chore(docs): cleanup code & docs

* chore(docs): better document present command

* chore(docs): improve gh page
2022-09-22 18:16:28 +02:00
cbee6320f5 chore(version): update version 2022-09-21 16:11:20 +02:00
382084f9ef feat(cli): record presentation (#25)
* feat(cli): record presentation

As proposed in #21, it is now possible to record a presentation output to a video file, with option `--record-to="some_file.avi"`.

Closes #21

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-21 16:10:37 +02:00
068484b828 chore(docs): add base cli docs 2022-09-21 16:09:08 +02:00
91f8d97acf chore(cli): show default for all cli options (#26) 2022-09-21 15:41:19 +02:00
49cdedc6fe chore(README): add link to doc 2022-09-21 15:36:38 +02:00
fe1fa059f6 chore(ci): auto build docs 2022-09-21 15:30:07 +02:00
3f6d2e5e57 chore(docs): create first documentation 2022-09-21 15:26:10 +02:00
99ad798155 chore(version): update version 2022-09-21 10:08:57 +02:00
84c25f1ed5 fix(ui): enhance window quality (#22)
* fix(ui): enhance window quality

This fix always resizes the frame size, as this seems to be a good cross-platform fix to the quality issue that occurs when the frame does not match the window size.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: remove unused import

* fix: actually only resize on Windows

* feat(cli): optional interpolation flag

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore: add image fox windows quality fix

* chore(README): document Windows fix

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-21 09:15:12 +02:00
7fb3fa01dd chore(ci): check for unused code with vulture (#23)
* chore(ci): check for unused code with vulture

* fix: add paths for vulture
2022-09-20 09:36:39 +02:00
2d2a225afe [pre-commit.ci] pre-commit autoupdate (#19)
updates:
- [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-20 09:18:14 +02:00
b9d2cd92b5 chore(docs): improve docstrings (#18) 2022-09-17 15:49:10 +02:00
620bb30960 feat(ui): much cleaner UI (#17) 2022-09-16 15:28:18 +02:00
b35a87befe chore(ci): add CodeQL check (#16)
* chore(ci): add CodeQL check

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-16 10:22:32 +02:00
84eb562f1b fix: remove dummy print 2022-09-15 15:21:02 +02:00
138cf014d2 fix: apply pypi's versioning 2022-09-15 15:12:17 +02:00
4816fc9a41 feat: large "present" feature refactoring (#15)
* fix(cli): previous presentation starts last slide

This changes the previous behavior that was such that going to previous presentation (scene) would start it from zero.

* fix(README): remove non-working svgs

Seems like GitHub does not support that

* wip: onto a cleaner parser, and more robust presentation

* fix(README): remove non-working svgs

Seems like GitHub does not support that

* wip: onto a cleaner parser, and more robust presentation

* wip: some progress

* fix: this is kinda working now

* fix: repeated reversed slide

* fix: import __version__ without importing package

* fix: typing list -> List

* chore(README): update features
2022-09-15 15:10:12 +02:00
f6f851bd09 chore(README): change link of example file 2022-09-14 18:59:28 +02:00
87dba671ac chore(README): add link to example file 2022-09-14 18:58:19 +02:00
c6c19bce89 misc(logo): update logo 2022-09-14 18:52:39 +02:00
58999d0681 fix(cli): previous presentation starts last slide (#14)
This changes the previous behavior that was such that going to previous presentation (scene) would start it from zero.
2022-09-14 18:35:52 +02:00
8696fca829 chore(README): upgrade documentation in README (#13)
* chore(README): upgrade in the README documentation

* chore(README): upgrade in the README documentation

* fix(README): typo in emoji

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(README): typo in emoji (bis)

* fix: toc link

* fix: few typos found by LT

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-14 16:11:27 +02:00
2856aeb89b Update __version__.py 2022-09-14 13:44:55 +02:00
2ba0d48ac1 fix(cli): resize only on fullscreen (#12)
* fix(cli): resize only on fullscreen

Closes #10

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-14 13:41:25 +02:00
5f730593fb fix: increase logo quality but ratio is the same 2022-09-14 11:59:20 +02:00
dfc5c9eb6c Merge branch 'add-logo' into main 2022-09-14 11:57:25 +02:00
14c17e1d24 fix: better logo resolution
1280x640, according to GitHub
2022-09-14 11:56:20 +02:00
449ff4cd00 misc: create Manim Slides logo (#11)
* misc: create Manim Slides logo

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-14 11:45:43 +02:00
606c521573 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-09-14 09:42:31 +00:00
b199fc7023 misc: create Manim Slides logo 2022-09-14 11:42:01 +02:00
4b05f22c8c fix(cli): disable printing "Manim Community v..." (#9) 2022-09-13 12:01:02 +02:00
1da3492732 chore(ci): test builds on multiple oses (#7)
* chore(ci): test builds on multiple oses

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: add tqdm to setup

* chore(ci): test builds on multiple oses

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: install deps in two steps

* fix(bug): allows shape to be 2 or 3 dims

* release new version

* chore(ci): test builds on multiple oses

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(ci): test builds on multiple oses

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: install deps in two steps

* fix: deps string

* fix: ci

* fix: single quote strings

* fix: choco install manimce

* Add pyopengl

* Update test_examples.yml

* fix: custom 3d example for manimgl

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: virtual frame buffer

* fix: add ffmpeg dep

* feat(cli): add skip-all option for testing

* chore(lint): black fmt

* fix: typo in deps

* fix: python -m pip install

* fix: typo

* Update test_examples.yml

* fix: try fix

* fix: pip install --user

* pip install -e

* only on windows

* fix: tmp fix

* fix: typo in int parsing

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update __version__.py

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-12 12:30:33 +02:00
0d97bdabb8 release new version 2022-09-11 12:26:46 +02:00
8e50adf0ba Merge pull request #8 from jeertmans/fix-shape
fix(bug): allows shape to be 2 or 3 dims
2022-09-11 12:26:01 +02:00
d583d591c1 fix(bug): allows shape to be 2 or 3 dims 2022-09-11 12:25:09 +02:00
c243fe3481 fix: add tqdm to setup 2022-09-11 12:14:54 +02:00
eba84a44fc fix(deps): remove manim dep 2022-09-10 23:10:42 +02:00
c8cf2e6074 Update README.md 2022-09-10 23:06:30 +02:00
743329c043 release 3.2 2022-09-10 23:00:26 +02:00
ed1b2eb698 Merge pull request #6 from jeertmans/manimgl
feat: add support for manimgl
2022-09-10 22:59:34 +02:00
cda304fef0 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-09-10 20:54:37 +00:00
3e6c562b1d Merge remote-tracking branch 'origin/manimgl' into manimgl 2022-09-10 22:54:29 +02:00
85c295a2c1 feat: add support for manimgl 2022-09-10 22:54:27 +02:00
ac5582753c feat: add support for manimgl 2022-09-10 22:53:35 +02:00
c53e410ff8 Update README.md 2022-09-08 00:13:01 +02:00
27 changed files with 1311 additions and 296 deletions

72
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '45 3 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

15
.github/workflows/languagetool.yml vendored Normal file
View File

@ -0,0 +1,15 @@
on:
pull_request:
workflow_dispatch:
name: LanguageTool check
jobs:
languagetool_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: reviewdog/action-languagetool@v1
with:
reporter: github-pr-review
level: warning

50
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,50 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Install Linux Dependencies
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg
- name: Install Python dependencies
run: pip install manim sphinx sphinx_click furo
- name: Install local Python package
run: pip install -e .
- name: Build docs
run: cd docs && make html
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
# Upload docs/build/html dir
path: 'docs/build/html/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

100
.github/workflows/test_examples.yml vendored Normal file
View File

@ -0,0 +1,100 @@
on:
pull_request:
paths:
- '**.py'
workflow_dispatch:
name: Test Examples
jobs:
build-examples:
strategy:
matrix:
manim: [manim, manimgl]
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.7', '3.8', '3.9', '3.10']
exclude:
# excludes manimgl on Windows because if throws errors
# related to OpenGL, which seems hard to fix:
# Your graphics drivers do not support OpenGL 2.0.
- os: windows-latest
manim: manimgl
# manimgl actually requires Python >= 3.8, see:
# https://github.com/3b1b/manim/issues/1808
- manim: manimgl
pyversion: '3.7'
# We only test Python 3.10 on Windows and MacOS
- os: windows-latest
pyversion: '3.7'
- os: windows-latest
pyversion: '3.8'
- os: windows-latest
pyversion: '3.9'
- os: macos-latest
pyversion: '3.7'
- os: macos-latest
pyversion: '3.8'
- os: macos-latest
pyversion: '3.9'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
- name: Append to Path on MacOS and Ubuntu
if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest'
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
- name: Append to Path on Windows
if: matrix.os == 'windows-latest'
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install MacOS dependencies (manim only)
if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
run: brew install py3cairo
- name: Install MacOS dependencies
if: matrix.os == 'macos-latest'
run: brew install ffmpeg
- name: Install Ubuntu dependencies
if: matrix.os == 'ubuntu-latest'
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
- name: Install manim on MacOs
if: matrix.manim == 'manim' && matrix.os == 'macos-latest'
run: pip3 install --user manim
- name: Install manim on Ubuntu and Windows
if: matrix.manim == 'manim' && (matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest')
run: python -m pip install --user manim
- name: Install manimgl on MacOs
if: matrix.manim == 'manimgl' && matrix.os == 'macos-latest'
run: pip3 install --user manimgl
- name: Install manimgl on Ubuntu and Windows
if: matrix.manim == 'manimgl' && matrix.os != 'macos-latest'
run: python -m pip install --user manimgl
- name: Install manim-slides on MacOS
if: matrix.os == 'macos-latest'
run: pip3 install --user .
- name: Install manim-slides on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: xvfb-run -a -s "-screen 0 1400x900x24" python -m pip install --user .
- name: Install manim-slides on Windows
if: matrix.os == 'windows-latest'
run: pip3 install -e .
- name: Build slides with manim
if: matrix.manim == 'manim'
run: python -m manim -ql example.py Example ThreeDExample
- name: Build slides with manimgl on Ubuntu
if: matrix.manim == 'manimgl' && matrix.os == 'ubuntu-latest'
run: xvfb-run -a -s "-screen 0 1400x900x24" manim-render -l example.py Example ThreeDExample
- name: Build slides with manimgl on MacOS or Windows
if: matrix.manim == 'manimgl' && (matrix.os == 'macos-latest' || matrix.os == 'windows-latest')
run: manimgl -l example.py Example ThreeDExample
- name: Test slides on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: xvfb-run -a -s "-screen 0 1400x900x24" manim-slides Example ThreeDExample --skip-all
- name: Test slides on MacOS or Windows
if: matrix.os == 'macos-latest' || matrix.os == 'windows-latest'
run: manim-slides Example ThreeDExample --skip-all

6
.gitignore vendored
View File

@ -13,3 +13,9 @@ __pycache__/
slides/
.manim-slides.json
videos/
images/
docs/build/

View File

@ -6,7 +6,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.6.0
rev: 22.8.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
@ -15,3 +15,11 @@ repos:
- id: isort
name: isort (python)
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 22.8.0
hooks:
- id: black
- repo: https://github.com/jendrikseipp/vulture
rev: v2.3
hooks:
- id: vulture

181
README.md
View File

@ -1,34 +1,76 @@
![Manim Slides Logo](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png)
[![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 either [manim-community](https://www.manim.community/). Currently, support for 3b1b's manim is not planned.
Tool for live presentations using either [Manim (community edition)](https://www.manim.community/) or [ManimGL](https://3b1b.github.io/manim/). Manim Slides will *automatically* detect the one you are using!
> **_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.
> **_NOTE:_** This project extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
## Install
- [Installation](#installation)
* [Dependencies](#dependencies)
* [Pip install](#pip-install)
* [Install From Repository](#install-from-repository)
- [Usage](#usage)
* [Basic Example](#basic-example)
* [Key Bindings](#key-bindings)
* [Other Examples](#other-examples)
- [Features and Comparison with Original manim-presentation](#features-and-comparison-with-original-manim-presentation)
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
- [Contributing](#contributing)
```
## Installation
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a [virtualenv](https://docs.python.org/3/tutorial/venv.html) for a local installation.
### Dependencies
Manim Slides requires either Manim or ManimGL to be installed. Having both packages installed is fine too.
If none of those packages are installed, please refer to their specifc installation guidelines:
- [Manim](https://docs.manim.community/en/stable/installation.html)
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
### Pip Install
The recommended way to install the latest release is to use pip:
```bash
pip install manim-slides
```
## Usage
### Install From Repository
Use the class `Slide` as your scenes base class:
```python
from manim_slides import Slide
An alternative way to install Manim Slides is to clone the git repository, and install from there:
class Example(Slide):
def construct(self):
...
```bash
git clone https://github.com/jeertmans/manim-slides
pip install -e .
```
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings).
> *Note:* the `-e` flag allows you to edit the files, and observe the changes directly when using Manim Slides
## Usage
Using Manim Slides is a two-step process:
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.pause()` everytime you want to create a new slide.
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
The command-line documentation is available [online](https://eertmans.be/manim-slides/).
### Basic Example
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
```python
# example.py
from manim import *
# or: from manimlib import *
from manim_slides import Slide
class Example(Slide):
@ -37,100 +79,113 @@ class Example(Slide):
dot = Dot()
self.play(GrowFromCenter(circle))
self.pause()
self.pause() # Waits user to press continue to go to the next slide
self.start_loop()
self.start_loop() # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.end_loop() # This will loop until user inputs a key
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.pause() # Waits user to press continue to go to the next slide
self.wait()
self.wait() # The presentation directly exits after last animation
```
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(...)`.
First, render the animation files:
```bash
manim example.py
# or
manimgl example.py
```
To start the presentation using `Scene1`, `Scene2` and so on simply run:
```
manim-slides Scene1 Scene2...
```bash
manim-slides [OPTIONS] Scene1 Scene2...
```
## Keybindings
Or in this example:
Default keybindings to control the presentation:
```bash
manim-slides Example
```
## Key Bindings
The default key bindings to control the presentation are:
| Keybinding | Action |
|:-----------:|:------------------------:|
| Right Arrow | Continue/Next Slide |
| Left Arrow | Previous Slide |
| R | Re-Animate Current Slide |
| R | Replay Current Slide |
| V | Reverse Current Slide |
| Spacebar | Play/Pause |
| Q | Quit |
You can run the **configuration wizard** to change those key bindings:
You can run the **configuration wizard** with:
```
```bash
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`.
Alternatively you can specify different key bindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REVERSE` `REWIND` and `PLAY_PAUSE`.
A default file can be created with:
```
```bash
manim-slides init
```
> **_NOTE:_** `manim-slides` uses `cv2.waitKeyEx()` to wait for keypresses, and directly registers the key code.
## Run Example
## Other Examples
Clone this repository:
```
git clone https://github.com/jeertmans/manim-slides.git
cd manim-slides
```
Install `manim` and `manim-slides`:
```
pip install manim manim-slides
```
Render the example scene:
```
manim -qh example.py
```
Run the presentation
```
manim-slides Example
```
Other examples are available in the [`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py) file, if you downloaded the git repository.
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`
## Features and 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:
Below is a non-exhaustive list of features:
- [x] Allowing multiple keys to control one action (useful when you use a laser pointer)
- [x] More robust config files checking
- [x] Dependencies are installed with the package
- [x] Only one cli (to rule them all)
- [x] User can easily generate dummy config file
- [x] Config file path can be manually set
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
- [x] Handle 3D scenes out of the box
- [ ] Generate docs online
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
| Feature | `manim-slides` | `manim-presentation` |
|:--------|:--------------:|:--------------------:|
| Support for Manim | :heavy_check_mark: | :heavy_check_mark: |
| Support for ManimGL | :heavy_check_mark: | :heavy_multiplication_x: |
| Configurable key bindings | :heavy_check_mark: | :heavy_check_mark: |
| Configurable paths | :heavy_check_mark: | :heavy_multiplication_x: |
| Play / Pause slides | :heavy_check_mark: | :heavy_check_mark: |
| Next / Previous slide | :heavy_check_mark: | :heavy_check_mark: |
| Replay slide | :heavy_check_mark: | :heavy_check_mark: |
| Reverse slide | :heavy_check_mark: | :heavy_multiplication_x: |
| Multiple key per actions | :heavy_check_mark: | :heavy_multiplication_x: |
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
## Contributions and license
## F.A.Q
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.
### How to increase quality on Windows
On Windows platform, one may encounter a lower image resolution than expected. Usually, this is observed because Windows rescales every application to fit the screen.
As found by [@arashash](https://github.com/arashash), in [#20](https://github.com/jeertmans/manim-slides/issues/20), the problem can be addressed by changing the scaling factor to 100%:
![Windows Fix Scaling](static/windows_quality_fix.png)
in *Settings*->*Display*.
## Contributing
Contributions are more than welcome!
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
[pypi-version-url]: https://pypi.org/project/manim-slides/

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

26
docs/source/conf.py Normal file
View File

@ -0,0 +1,26 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Manim Slides"
copyright = "2022, Jérome Eertmans"
author = "Jérome Eertmans"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx.ext.autodoc", "sphinx_click"]
templates_path = ["_templates"]
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_static_path = ["_static"]

20
docs/source/index.rst Normal file
View File

@ -0,0 +1,20 @@
.. manim-slides documentation master file, created by
sphinx-quickstart on Wed Sep 21 15:07:28 2022.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. image:: _static/logo.png
:width: 600
:align: center
:alt: Manim Slide logo
Welcome to Manim Slide's CLI documentation!
===========================================
This page contains an exhaustive list of all the commands available with `manim-slides`.
If you need help installing or using Manim Slide, please refer to the `GitHub README <https://github.com/jeertmans/manim-slides>`_.
.. click:: manim_slides.main:cli
:prog: manim-slides
:nested: full

View File

@ -1,4 +1,15 @@
from manim import *
import sys
if "manim" in sys.modules:
from manim import *
MANIMGL = False
elif "manimlib" in sys.modules:
from manimlib import *
MANIMGL = True
else:
raise ImportError("This script must be run with either `manim` or `manimgl`")
from manim_slides import Slide, ThreeDSlide
@ -29,37 +40,88 @@ class Example(Slide):
self.play(dot.animate.move_to(ORIGIN))
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
# For ThreeDExample, things are different
self.add(axes)
if not MANIMGL:
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.add(axes)
self.pause()
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.pause()
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.play(dot.animate.move_to(ORIGIN))
self.pause()
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))
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))
else:
# WARNING: 3b1b's manim change how ThreeDScene work,
# this is why things have to be managed differently.
class ThreeDExample(Slide):
CONFIG = {
"camera_class": ThreeDCamera,
}
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.add(axes)
frame = self.camera.frame
frame.set_euler_angles(
theta=30 * DEGREES,
phi=75 * DEGREES,
gamma=0,
)
self.play(GrowFromCenter(circle))
updater = lambda m, dt: m.increment_theta((75 * DEGREES / 4) * dt)
frame.add_updater(updater)
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
frame.remove_updater(updater)
self.play(frame.animate.set_theta(30 * DEGREES))
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))

View File

@ -1 +1 @@
__version__ = "3.1.0"
__version__ = "4.1.1"

View File

@ -1,9 +1,12 @@
from typing import Callable
import click
from .defaults import CONFIG_PATH
def config_path_option(function):
def config_path_option(function) -> Callable:
"""Wraps a function to add configuration path option."""
return click.option(
"-c",
"--config",
@ -11,10 +14,12 @@ def config_path_option(function):
default=CONFIG_PATH,
type=click.Path(dir_okay=False),
help="Set path to configuration file.",
show_default=True,
)(function)
def config_options(function):
def config_options(function) -> Callable:
"""Wraps a function to add configuration options."""
function = config_path_option(function)
function = click.option(
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."

View File

@ -1,4 +1,6 @@
from typing import Optional, Set
import os
from enum import Enum
from typing import List, Optional, Set
from pydantic import BaseModel, root_validator, validator
@ -6,6 +8,8 @@ from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
class Key(BaseModel):
"""Represents a list of key codes, with optionally a name."""
ids: Set[int]
name: Optional[str] = None
@ -20,6 +24,8 @@ class Key(BaseModel):
class Config(BaseModel):
"""General Manim Slides config"""
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")
@ -47,3 +53,88 @@ class Config(BaseModel):
key.name = other_key.name or key.name
return self
class SlideType(str, Enum):
slide = "slide"
loop = "loop"
last = "last"
class SlideConfig(BaseModel):
type: SlideType
start_animation: int
end_animation: int
number: int
terminated: bool = False
@validator("start_animation", "end_animation")
def index_is_posint(cls, v: int):
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
return v
@validator("number")
def number_is_strictly_posint(cls, v: int):
if v <= 0:
raise ValueError("Slide number cannot be negative or zero")
return v
@root_validator
def start_animation_is_before_end(cls, values):
if values["start_animation"] >= values["end_animation"]:
if values["start_animation"] == values["end_animation"] == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting."
)
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return values
def is_slide(self):
return self.type == SlideType.slide
def is_loop(self):
return self.type == SlideType.loop
def is_last(self):
return self.type == SlideType.last
class PresentationConfig(BaseModel):
slides: List[SlideConfig]
files: List[str]
@validator("files", pre=True, each_item=True)
def is_file_and_exists(cls, v):
if not os.path.exists(v):
raise ValueError(
f"Animation file {v} does not exist. Are you in the right directory?"
)
if not os.path.isfile(v):
raise ValueError(f"Animation file {v} is not a file")
return v
@root_validator
def animation_indices_match_files(cls, values):
files = values.get("files")
slides = values.get("slides")
if files is None or slides is None:
return values
n_files = len(files)
for slide in slides:
if slide.end_animation > n_files:
raise ValueError(
f"The following slide's contains animations not listed in files {files}: {slide}"
)
return values

View File

@ -1,5 +1,8 @@
import platform
import cv2
FONT_ARGS = (cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 1, cv2.LINE_AA)
FOLDER_PATH: str = "./slides"
CONFIG_PATH: str = ".manim-slides.json"

View File

@ -10,6 +10,11 @@ from .wizard import init, wizard
@click.version_option(__version__, "-v", "--version")
@click.help_option("-h", "--help")
def cli():
"""
Manim Slides command-line utilities.
If not command is specified, defaults to `present`.
"""
pass

66
manim_slides/manim.py Normal file
View File

@ -0,0 +1,66 @@
import os
import sys
from contextlib import contextmanager
from importlib.util import find_spec
@contextmanager
def suppress_stdout():
with open(os.devnull, "w") as devnull:
old_stdout = sys.stdout
sys.stdout = devnull
try:
yield
finally:
sys.stdout = old_stdout
MANIM_PACKAGE_NAME = "manim"
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
MANIMGL_PACKAGE_NAME = "manimlib"
MANIMGL_AVAILABLE = find_spec(MANIMGL_PACKAGE_NAME) is not None
MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
if MANIM_IMPORTED and MANIMGL_IMPORTED:
from manim import logger
logger.warn(
"Both manim and manimgl are installed, therefore `manim-slide` needs to need which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
)
MANIM = True
MANIMGL = False
elif MANIM_IMPORTED:
MANIM = True
MANIMGL = False
elif MANIMGL_IMPORTED:
MANIM = False
MANIMGL = True
elif MANIM_AVAILABLE:
MANIM = True
MANIMGL = False
elif MANIMGL_AVAILABLE:
MANIM = False
MANIMGL = True
else:
raise ModuleNotFoundError(
"Either manim (community) or manimgl (3b1b) package must be installed"
)
FFMPEG_BIN = None
if MANIMGL:
from manimlib import Scene, ThreeDScene, config
from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log as logger
else:
with suppress_stdout(): # Avoids printing "Manim Community v..."
from manim import Scene, ThreeDScene, config, logger
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable

View File

@ -5,131 +5,229 @@ import platform
import sys
import time
from enum import IntEnum, auto, unique
if platform.system() == "Windows":
import ctypes
from typing import List, Tuple
import click
import cv2
import numpy as np
from pydantic import ValidationError
from tqdm import tqdm
from .commons import config_path_option
from .config import Config
from .defaults import CONFIG_PATH, FOLDER_PATH
from .slide import reverse_video_path
from .config import Config, PresentationConfig, SlideConfig, SlideType
from .defaults import CONFIG_PATH, FOLDER_PATH, FONT_ARGS
INTERPOLATION_FLAGS = {
"nearest": cv2.INTER_NEAREST,
"linear": cv2.INTER_LINEAR,
"cubic": cv2.INTER_CUBIC,
"area": cv2.INTER_AREA,
"lanczos4": cv2.INTER_LANCZOS4,
"linear-exact": cv2.INTER_LINEAR_EXACT,
"nearest-exact": cv2.INTER_NEAREST_EXACT,
}
WINDOW_NAME = "Manim Slides"
WINDOW_INFO_NAME = f"{WINDOW_NAME}: Info"
WINDOWS = platform.system() == "Windows"
@unique
class State(IntEnum):
"""Represents all possible states of a slide presentation."""
PLAYING = auto()
PAUSED = auto()
WAIT = auto()
END = auto()
def __str__(self):
def __str__(self) -> str:
return self.name.capitalize()
def now() -> int:
"""Returns time.time() in milliseconds."""
return round(time.time() * 1000)
def fix_time(x: float) -> float:
return x if x > 0 else 1
def fix_time(t: float) -> float:
"""Clips time t such that it is always positive."""
return t if t > 0 else 1
class Presentation:
def __init__(self, config, last_frame_next: bool = False):
self.last_frame_next = last_frame_next
self.slides = config["slides"]
self.files = [path for path in config["files"]]
"""Creates presentation from a configuration object."""
def __init__(self, config: PresentationConfig):
self.slides: List[SlideConfig] = config.slides
self.files: List[str] = config.files
self.current_slide_index = 0
self.current_animation = self.current_slide.start_animation
self.current_file = None
self.loaded_animation_cap = -1
self.cap = None # cap = cv2.VideoCapture
self.reverse = False
self.reversed_slide = -1
self.reversed_animation = -1
self.lastframe = []
self.lastframe = None
self.caps = [None for _ in self.files]
self.reset()
self.add_last_slide()
@property
def current_slide(self) -> SlideConfig:
"""Returns currently playing slide."""
return self.slides[self.current_slide_index]
@property
def first_slide(self) -> SlideConfig:
"""Returns first slide."""
return self.slides[0]
@property
def last_slide(self) -> SlideConfig:
"""Returns last slide."""
return self.slides[-1]
def release_cap(self):
"""Releases current Video Capture, if existing."""
if not self.cap is None:
self.cap.release()
self.loaded_animation_cap = -1
def load_animation_cap(self, animation: int):
"""Loads video file of given animation."""
# We must load a new VideoCapture file if:
if (self.loaded_animation_cap != animation) or (
self.reverse and self.reversed_animation != animation
): # cap already loaded
self.release_cap()
file = self.files[animation]
if self.reverse:
file = "{}_reversed{}".format(*os.path.splitext(file))
self.reversed_animation = animation
self.current_file = file
self.cap = cv2.VideoCapture(file)
self.loaded_animation_cap = animation
@property
def current_cap(self) -> cv2.VideoCapture:
"""Returns current VideoCapture object."""
self.load_animation_cap(self.current_animation)
return self.cap
def rewind_current_slide(self):
"""Rewinds current slide to first frame."""
if self.reverse:
self.current_animation = self.current_slide.end_animation - 1
else:
self.current_animation = self.current_slide.start_animation
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def cancel_reverse(self):
"""Cancels any effet produced by a reversed slide."""
if self.reverse:
self.reverse = False
self.reversed_animation = -1
self.release_cap()
def reverse_current_slide(self):
"""Reverses current slide."""
self.reverse = True
self.rewind_current_slide()
def load_next_slide(self):
"""Loads next slide."""
if self.reverse:
self.cancel_reverse()
self.rewind_current_slide()
elif self.current_slide.is_last():
self.current_slide.terminated = True
else:
self.current_slide_index = min(
len(self.slides) - 1, self.current_slide_index + 1
)
self.rewind_current_slide()
def load_previous_slide(self):
"""Loads previous slide."""
self.cancel_reverse()
self.current_slide_index = max(0, self.current_slide_index - 1)
self.rewind_current_slide()
@property
def fps(self) -> int:
"""Returns the number of frames per second of the current video."""
return self.current_cap.get(cv2.CAP_PROP_FPS)
def add_last_slide(self):
last_slide_end = self.slides[-1]["end_animation"]
last_animation = len(self.files)
"""Add a 'last' slide to the end of slides."""
self.slides.append(
dict(
start_animation=last_slide_end,
end_animation=last_animation,
type="last",
number=len(self.slides) + 1,
terminated=False,
SlideConfig(
start_animation=self.last_slide.end_animation,
end_animation=self.last_slide.end_animation + 1,
type=SlideType.last,
number=self.last_slide.number + 1,
)
)
def reset(self):
"""Rests current presentation."""
self.current_animation = 0
self.load_this_cap(0)
self.current_slide_i = 0
self.slides[-1]["terminated"] = False
self.load_animation_cap(0)
self.current_slide_index = 0
self.slides[-1].terminated = False
def next(self):
if self.current_slide["type"] == "last":
self.current_slide["terminated"] = True
def load_last_slide(self):
"""Loads last slide."""
self.current_slide_index = len(self.slides) - 2
assert (
self.current_slide_index >= 0
), "Slides should be at list of a least two elements"
self.current_animation = self.current_slide.start_animation
self.load_animation_cap(self.current_animation)
self.slides[-1].terminated = False
@property
def next_animation(self) -> int:
"""Returns the next animation."""
if self.reverse:
return self.current_animation - 1
else:
self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1)
self.rewind_slide()
def prev(self):
self.current_slide_i = max(0, self.current_slide_i - 1)
self.rewind_slide()
def reverse_slide(self):
self.rewind_slide(reverse=True)
def rewind_slide(self, reverse: bool = False):
self.reverse = reverse
self.current_animation = self.current_slide["start_animation"]
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def load_this_cap(self, cap_number: int):
if (
self.caps[cap_number] is None
or (self.reverse and self.reversed_slide != cap_number)
or (not self.reverse and self.reversed_slide == cap_number)
):
# unload other caps
for i in range(len(self.caps)):
if not self.caps[i] is None:
self.caps[i].release()
self.caps[i] = None
# load this cap
file = self.files[cap_number]
if self.reverse:
self.reversed_slide = cap_number
file = "{}_reversed{}".format(*os.path.splitext(file))
else:
self.reversed_slide = -1
self.caps[cap_number] = cv2.VideoCapture(file)
return self.current_animation + 1
@property
def current_slide(self):
return self.slides[self.current_slide_i]
def is_last_animation(self) -> int:
"""Returns True if current animation is the last one of current slide."""
if self.reverse:
return self.current_animation == self.current_slide.start_animation
else:
return self.next_animation == self.current_slide.end_animation
@property
def current_cap(self):
self.load_this_cap(self.current_animation)
return self.caps[self.current_animation]
def current_frame_number(self) -> int:
"""Returns current frame number."""
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
@property
def fps(self):
return self.current_cap.get(cv2.CAP_PROP_FPS)
def update_state(self, state) -> Tuple[np.ndarray, State]:
"""
Updates the current state given the previous one.
# 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):
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.
"""
if state == State.PAUSED:
if len(self.lastframe) == 0:
if self.lastframe is None:
_, self.lastframe = self.current_cap.read()
return self.lastframe, state
still_playing, frame = self.current_cap.read()
@ -137,36 +235,30 @@ class Presentation:
self.lastframe = frame
elif state in [state.WAIT, state.PAUSED]:
return self.lastframe, state
elif self.current_slide["type"] == "last" and self.current_slide["terminated"]:
elif self.current_slide.is_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()
else: # not still playing
if self.is_last_animation:
if self.current_slide.is_slide():
state = State.WAIT
elif self.current_slide["type"] == "loop":
self.current_animation = self.current_slide["start_animation"]
state = State.PLAYING
self.rewind_slide()
elif self.current_slide["type"] == "last":
self.current_slide["terminated"] = True
elif self.current_slide.is_loop():
if self.reverse:
state = State.WAIT
else:
self.current_animation = self.current_slide.start_animation
state = State.PLAYING
self.rewind_current_slide()
elif self.current_slide.is_last():
self.current_slide.terminated = True
elif (
self.current_slide["type"] == "last"
and self.current_slide["end_animation"] == self.current_animation
self.current_slide.is_last()
and self.current_slide.end_animation == self.current_animation
):
state = State.WAIT
else:
# Play next video!
self.current_animation += 1
self.load_this_cap(self.current_animation)
self.current_animation = self.next_animation
self.load_animation_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)
@ -174,46 +266,64 @@ class Presentation:
class Display:
def __init__(self, presentations, config, start_paused=False, fullscreen=False):
"""Displays one or more presentations one after each other."""
def __init__(
self,
presentations,
config,
start_paused=False,
fullscreen=False,
skip_all=False,
resolution=(1980, 1080),
interpolation_flag=cv2.INTER_LINEAR,
record_to=None,
):
self.presentations = presentations
self.start_paused = start_paused
self.config = config
self.skip_all = skip_all
self.fullscreen = fullscreen
self.resolution = resolution
self.interpolation_flag = interpolation_flag
self.record_to = record_to
self.recordings = []
self.window_flags = (
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_NORMAL
)
self.state = State.PLAYING
self.lastframe = None
self.current_presentation_i = 0
self.current_presentation_index = 0
self.exit = False
self.lag = 0
self.last_time = now()
if platform.system() == "Windows":
user32 = ctypes.windll.user32
self.screen_width, self.screen_height = user32.GetSystemMetrics(
0
), user32.GetSystemMetrics(1)
cv2.namedWindow(
WINDOW_INFO_NAME,
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_AUTOSIZE,
)
if fullscreen:
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty(
"Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
if self.fullscreen:
cv2.namedWindow(
WINDOW_NAME, cv2.WINDOW_GUI_NORMAL | cv2.WND_PROP_FULLSCREEN
)
def resize_frame_to_screen(self, frame: np.ndarray):
frame_height, frame_width = frame.shape
scale_height = self.screen_height / frame_height
scale_width = self.screen_width / frame_width
scale = min(scale_height, scale_width)
return cv2.resize(frame, (int(scale * frame_height, scale * frame_width)))
cv2.setWindowProperty(
WINDOW_NAME, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
)
else:
cv2.namedWindow(WINDOW_NAME, self.window_flags)
cv2.resizeWindow(WINDOW_NAME, *self.resolution)
@property
def current_presentation(self):
return self.presentations[self.current_presentation_i]
def current_presentation(self) -> Presentation:
"""Returns the current presentation."""
return self.presentations[self.current_presentation_index]
def run(self):
while True:
"""Runs a series of presentations until end or exit."""
while not self.exit:
self.lastframe, self.state = self.current_presentation.update_state(
self.state
)
@ -222,29 +332,49 @@ class Display:
self.state = State.PAUSED
self.start_paused = False
if self.state == State.END:
if self.current_presentation_i == len(self.presentations) - 1:
if self.current_presentation_index == len(self.presentations) - 1:
self.quit()
continue
else:
self.current_presentation_i += 1
self.current_presentation_index += 1
self.state = State.PLAYING
self.handle_key()
if self.exit:
continue
self.show_video()
self.show_info()
def show_video(self):
"""Shows updated video."""
self.lag = now() - self.last_time
self.last_time = now()
if not self.record_to is None:
pres = self.current_presentation
self.recordings.append(
(pres.current_file, pres.current_frame_number, pres.fps)
)
frame = self.lastframe
if platform.system() == "Windows":
frame = self.resize_frame_to_screen(frame)
# If Window was manually closed (impossible in fullscreen),
# we reopen it
if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
cv2.namedWindow(WINDOW_NAME, self.window_flags)
cv2.resizeWindow(WINDOW_NAME, *self.resolution)
cv2.imshow("Video", frame)
if WINDOWS: # Only resize on Windows
_, _, w, h = cv2.getWindowImageRect(WINDOW_NAME)
if (h, w) != frame.shape[:2]: # Only if shape is different
frame = cv2.resize(frame, (w, h), self.interpolation_flag)
cv2.imshow(WINDOW_NAME, frame)
def show_info(self):
"""Shows updated information about presentations."""
info = np.zeros((130, 420), np.uint8)
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
font_args = (FONT_ARGS[0], 0.7, *FONT_ARGS[2:])
grid_x = [30, 230]
grid_y = [30, 70, 110]
@ -258,27 +388,28 @@ class Display:
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
f"Slide {self.current_presentation.current_slide.number}/{len(self.current_presentation.slides)}",
(grid_x[0], grid_y[1]),
*font_args,
)
cv2.putText(
info,
f"Slide Type: {self.current_presentation.current_slide['type']}",
f"Slide Type: {self.current_presentation.current_slide.type}",
(grid_x[1], grid_y[1]),
*font_args,
)
cv2.putText(
info,
f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}",
f"Scene {self.current_presentation_index + 1}/{len(self.presentations)}",
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
*font_args,
)
cv2.imshow("Info", info)
cv2.imshow(WINDOW_INFO_NAME, info)
def handle_key(self):
"""Handles key strokes."""
sleep_time = math.ceil(1000 / self.current_presentation.fps)
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
@ -291,28 +422,64 @@ class Display:
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.load_next_slide()
self.state = State.PLAYING
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
self.current_presentation.next()
elif (
self.state == State.PLAYING and self.config.CONTINUE.match(key)
) or self.skip_all:
self.current_presentation.load_next_slide()
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()
if self.current_presentation.current_slide_index == 0:
if self.current_presentation_index == 0:
self.current_presentation.load_previous_slide()
else:
self.current_presentation_index -= 1
self.current_presentation.load_last_slide()
self.state = State.PLAYING
else:
self.current_presentation.prev()
self.current_presentation.load_previous_slide()
self.state = State.PLAYING
elif self.config.REVERSE.match(key):
self.current_presentation.reverse_slide()
self.current_presentation.reverse_current_slide()
self.state = State.PLAYING
elif self.config.REWIND.match(key):
self.current_presentation.rewind_slide()
self.current_presentation.cancel_reverse()
self.current_presentation.rewind_current_slide()
self.state = State.PLAYING
def quit(self):
"""Destroys all windows created by presentations and exits gracefully."""
cv2.destroyAllWindows()
sys.exit()
if not self.record_to is None and len(self.recordings) > 0:
file, frame_number, fps = self.recordings[0]
cap = cv2.VideoCapture(file)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
_, frame = cap.read()
w, h = frame.shape[:2]
fourcc = cv2.VideoWriter_fourcc(*"XVID")
out = cv2.VideoWriter(self.record_to, fourcc, fps, (h, w))
out.write(frame)
for _file, frame_number, _ in tqdm(
self.recordings[1:], desc="Creating recording file", leave=False
):
if file != _file:
cap.release()
file = _file
cap = cv2.VideoCapture(_file)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
_, frame = cap.read()
out.write(frame)
cap.release()
out.release()
self.exit = True
@click.command()
@ -321,6 +488,7 @@ class Display:
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
show_default=True,
)
@click.help_option("-h", "--help")
def list_scenes(folder):
@ -330,12 +498,18 @@ def list_scenes(folder):
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder):
def _list_scenes(folder) -> List[str]:
"""Lists available scenes in given directory."""
scenes = []
for file in os.listdir(folder):
if file.endswith(".json"):
scenes.append(os.path.basename(file)[:-5])
try:
filepath = os.path.join(folder, file)
_ = PresentationConfig.parse_file(filepath)
scenes.append(os.path.basename(file)[:-5])
except Exception: # Could not parse this file as a proper presentation config
pass
return scenes
@ -348,17 +522,58 @@ def _list_scenes(folder):
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
show_default=True,
)
@click.option("--start-paused", is_flag=True, help="Start paused.")
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
@click.option(
"--last-frame-next",
"--skip-all",
is_flag=True,
help="Show the next animation first frame as last frame (hack).",
help="Skip all slides, useful the test if slides are working.",
)
@click.option(
"-r",
"--resolution",
type=(int, int),
default=(1920, 1080),
help="Window resolution WIDTH HEIGHT used if fullscreen is not set. You may manually resize the window afterward.",
show_default=True,
)
@click.option(
"-i",
"--interpolation-flag",
type=click.Choice(INTERPOLATION_FLAGS.keys(), case_sensitive=False),
default="linear",
help="Set the interpolation flag to be used when resizing image. See OpenCV cv::InterpolationFlags.",
show_default=True,
)
@click.option(
"--record-to",
type=click.Path(dir_okay=False),
default=None,
help="If set, the presentation will be recorded into a AVI video file with given name.",
)
@click.help_option("-h", "--help")
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
"""Present the different scenes."""
def present(
scenes,
config_path,
folder,
start_paused,
fullscreen,
skip_all,
resolution,
interpolation_flag,
record_to,
):
"""
Present SCENE(s), one at a time, in order.
Each SCENE parameter must be the name of a Manim scene, with existing SCENE.json config file.
You can present the same SCENE multiple times by repeating the parameter.
Use `manim-slide list-scenes` to list all available scenes in a given folder.
"""
if len(scenes) == 0:
scene_choices = _list_scenes(folder)
@ -377,36 +592,60 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
indices = list(map(int, value.strip().replace(" ", "").split(",")))
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
raise ValueError("Please only enter numbers displayed on the screen.")
raise click.UsageError(
"Please only enter numbers displayed on the screen."
)
return [scene_choices[i] for i in indices]
if len(scene_choices) == 0:
raise ValueError("No scenes were found, are you in the correct directory?")
raise click.UsageError(
"No scenes were found, are you in the correct directory?"
)
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
break
except ValueError as e:
click.secho(e, fg="red")
raise click.UsageError(e)
presentations = list()
for scene in scenes:
config_file = os.path.join(folder, f"{scene}.json")
if not os.path.exists(config_file):
raise Exception(
raise click.UsageError(
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
)
config = json.load(open(config_file))
presentations.append(Presentation(config, last_frame_next=last_frame_next))
try:
config = PresentationConfig.parse_file(config_file)
presentations.append(Presentation(config))
except ValidationError as e:
raise click.UsageError(str(e))
if os.path.exists(config_path):
config = Config.parse_file(config_path)
try:
config = Config.parse_file(config_path)
except ValidationError as e:
raise click.UsageError(str(e))
else:
config = Config()
if not record_to is None:
_, ext = os.path.splitext(record_to)
if ext.lower() != ".avi":
raise click.UsageError(
f"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
)
display = Display(
presentations, config=config, start_paused=start_paused, fullscreen=fullscreen
presentations,
config=config,
start_paused=start_paused,
fullscreen=fullscreen,
skip_all=skip_all,
resolution=resolution,
interpolation_flag=INTERPOLATION_FLAGS[interpolation_flag],
record_to=record_to,
)
display.run()

View File

@ -3,32 +3,41 @@ import os
import platform
import shutil
import subprocess
from typing import List
from manim import Scene, ThreeDScene, config, logger
from tqdm import tqdm
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN as ffmpeg_executable
except ImportError:
ffmpeg_executable = config.ffmpeg_executable
from .config import PresentationConfig, SlideConfig, SlideType
from .defaults import FOLDER_PATH
def reverse_video_path(src: str) -> str:
file, ext = os.path.splitext(src)
return f"{file}_reversed{ext}"
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
def reverse_video_file(src: str, dst: str):
command = [config.ffmpeg_executable, "-i", src, "-vf", "reverse", dst]
"""Reverses a video file, writting the result to `dst`."""
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate()
class Slide(Scene):
"""
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
"""
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
if MANIMGL:
if not os.path.isdir("videos"):
os.mkdir("videos")
kwargs["file_writer_config"] = {
"break_into_partial_movies": True,
"output_directory": "",
"write_to_movie": True,
}
kwargs["preview"] = False
super().__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides = list()
self.current_slide = 1
@ -36,14 +45,48 @@ class Slide(Scene):
self.loop_start_animation = None
self.pause_start_animation = 0
@property
def partial_movie_files(self) -> List[str]:
"""Returns a list of partial movie files, a.k.a animations."""
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
@property
def show_progress_bar(self) -> bool:
"""Returns True if progress bar should be displayed."""
if MANIMGL:
return getattr(super(Scene, self), "show_progress_bar", True)
else:
return config["progress_bar"] != "none"
@property
def leave_progress_bar(self):
"""Returns True if progress bar should be left after completed."""
if MANIMGL:
return getattr(super(Scene, self), "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
def play(self, *args, **kwargs):
"""Overloads `self.play` and increment animation count."""
super().play(*args, **kwargs)
self.current_animation += 1
def pause(self):
"""Creates a new slide with previous animations."""
self.slides.append(
dict(
type="slide",
SlideConfig(
type=SlideType.slide,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
@ -53,16 +96,18 @@ class Slide(Scene):
self.pause_start_animation = self.current_animation
def start_loop(self):
"""Starts a loop."""
assert self.loop_start_animation is None, "You cannot nest loops"
self.loop_start_animation = self.current_animation
def end_loop(self):
"""Ends an existing loop."""
assert (
self.loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
dict(
type="loop",
SlideConfig(
type=SlideType.loop,
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
@ -72,15 +117,12 @@ class Slide(Scene):
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().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
def save_slides(self, use_cache=True):
"""
Saves slides, optionally using cached files.
Note that cached files only work with Manim.
"""
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
@ -95,21 +137,22 @@ class Slide(Scene):
if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder)
elif not use_cache:
shutil.rmtree(scene_files_folder)
os.mkdir(scene_files_folder)
else:
old_animation_files.update(os.listdir(scene_files_folder))
files = list()
for src_file in tqdm(
self.renderer.file_writer.partial_movie_files,
self.partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=config["progress_bar"] == "leave",
leave=self.leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=config["progress_bar"] == "none",
disable=not self.show_progress_bar,
):
filename = os.path.basename(src_file)
_hash, ext = os.path.splitext(filename)
rev_filename = f"{_hash}_reversed{ext}"
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
dst_file = os.path.join(scene_files_folder, filename)
# We only copy animation if it was not present
@ -133,13 +176,36 @@ class Slide(Scene):
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
f = open(slide_path, "w")
json.dump(dict(slides=self.slides, files=files), f)
f.close()
with open(slide_path, "w") as f:
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
)
def run(self, *args, **kwargs):
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
def render(self, *args, **kwargs):
"""MANIM render"""
# We need to disable the caching limit since we rely on intermidiate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
super().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
self.save_slides()
class ThreeDSlide(Slide, ThreeDScene):
"""
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
Note that ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
"""
pass

View File

@ -7,22 +7,38 @@ import numpy as np
from .commons import config_options
from .config import Config
from .defaults import CONFIG_PATH
from .defaults import CONFIG_PATH, FONT_ARGS
WINDOW_NAME = "Manim Slides Configuration Wizard"
WINDOW_SIZE = (120, 620)
def center_text_horizontally(text, window_size, font_args) -> int:
"""Returns centered position for text to be displayed in current window."""
_, width = window_size
font, scale, _, thickness, _ = font_args
(size_in_pixels, _), _ = cv2.getTextSize(text, font, scale, thickness)
return (width - size_in_pixels) // 2
def prompt(question: str) -> int:
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
display = np.zeros((130, 420), np.uint8)
"""Diplays some question in current window and waits for key press."""
display = np.zeros(WINDOW_SIZE, np.uint8)
cv2.putText(display, "* Manim Slides Wizard *", (70, 33), *font_args)
cv2.putText(display, question, (30, 85), *font_args)
text = "* Manim Slides Wizard *"
text_org = center_text_horizontally(text, WINDOW_SIZE, FONT_ARGS), 33
question_org = center_text_horizontally(question, WINDOW_SIZE, FONT_ARGS), 85
cv2.imshow("Manim Slides Configuration Wizard", display)
cv2.putText(display, "* Manim Slides Wizard *", text_org, *FONT_ARGS)
cv2.putText(display, question, question_org, *FONT_ARGS)
cv2.imshow(WINDOW_NAME, display)
return cv2.waitKeyEx(-1)
@click.command()
@config_options
@click.help_option("-h", "--help")
def wizard(config_path, force, merge):
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@ -30,12 +46,14 @@ def wizard(config_path, force, merge):
@click.command()
@config_options
@click.help_option("-h", "--help")
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):
"""Actual initialization code for configuration file, with optional interactive mode."""
if os.path.exists(config_path):
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
@ -61,6 +79,11 @@ def _init(config_path, force, merge, skip_interactive=False):
if not skip_interactive:
cv2.namedWindow(
WINDOW_NAME,
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_AUTOSIZE,
)
prompt("Press any key to continue")
for _, key in config:
@ -70,6 +93,6 @@ def _init(config_path, force, merge, skip_interactive=False):
config = Config.parse_file(config_path).merge_with(config)
with open(config_path, "w") as config_file:
config_file.write(config.json(indent=4))
config_file.write(config.json(indent=2))
click.echo(f"Configuration file successfully save to `{config_path}`")

2
pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.vulture]
paths = ["manim_slides"]

View File

@ -1,18 +1,25 @@
import importlib.util
import os
import sys
import setuptools
from manim_slides import __version__ as version
if sys.version_info < (3, 7):
raise RuntimeError("This package requires Python 3.7+")
spec = importlib.util.spec_from_file_location(
"__version__", os.path.join("manim_slides", "__version__.py")
)
version = importlib.util.module_from_spec(spec)
spec.loader.exec_module(version)
with open("README.md", "r") as f:
long_description = f.read()
setuptools.setup(
name="manim-slides",
version=version,
version=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",
@ -30,9 +37,9 @@ setuptools.setup(
"click>=8.0",
"click-default-group>=1.2",
"numpy>=1.19.3",
"manim",
"pydantic>=1.9.1",
"opencv-python>=4.6",
"tqdm>=4.62.3",
],
classifiers=[
"Programming Language :: Python :: 3",

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

39
static/logo.py Normal file
View File

@ -0,0 +1,39 @@
from manim import *
class ManimSlidesLogo(Scene):
def construct(self):
tex_template = TexTemplate()
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
self.camera.background_color = "#ffffff"
logo_green = "#87c2a5"
logo_blue = "#525893"
logo_red = "#e07a5f"
logo_black = "#343434"
ds_m = MathTex(r"\mathbb{M}", fill_color=logo_black).scale(7)
ds_m.shift(2.25 * LEFT + 1.5 * UP)
slides = MathTex(r"\mathbb{S}\text{lides}", fill_color=logo_black).scale(4)
slides.next_to(ds_m, DOWN)
slides.shift(DOWN)
play = Tex(
r"\faStepBackward\faStepForward",
fill_color=logo_black,
tex_template=tex_template,
).scale(4)
play.next_to(ds_m, LEFT)
play.shift(LEFT + 0.5 * DOWN)
comment = Tex(
r"\reflectbox{\faComment*[regular]}",
fill_color=logo_black,
tex_template=tex_template,
).scale(9)
comment.move_to(play)
comment.shift(0.4 * DOWN)
circle = Circle(color=logo_green, fill_opacity=1).shift(LEFT)
square = Square(color=logo_blue, fill_opacity=1).shift(UP)
triangle = Triangle(color=logo_red, fill_opacity=1).shift(RIGHT)
logo = VGroup(
triangle, square, circle, ds_m, slides, comment, play
) # order matters
logo.move_to(ORIGIN)
self.add(logo)

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB