Compare commits

...

181 Commits

Author SHA1 Message Date
9810425ff2 chore(version): update version 2022-12-07 17:19:19 +01:00
3dc543e3a6 chore(docs): update README and fix typo 2022-12-07 17:17:07 +01:00
c0c73ad4d4 feat(cli): feally featured RevealJS template (#80)
* feat(cli): feally featured RevealJS template

This adds an option for every possible RevealJS option. Error messages are also improved.

RevealJS version is bumped to latest (4.4.0).

* feat(cli): add primitive support for arbitrary JS functions

* fix(cli): some typos / issues in template

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

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

* fix(lib): pickling issue

* fix(lib): ignore typing error due to __reduce_ex__

* feat(cli): add template selection, fixes first slide bug, and rm stuff

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-07 16:27:23 +01:00
a82ca81dc5 chore(deps): add dependency groups (#89)
This uses poetry's dependency groups to be more rigorous about modules version.
2022-12-06 14:56:18 +01:00
a68a4e1517 feat(cli): speedup scaling with painter scaling (#88)
As suggested by PySide6's documentation, we avoid using pixmap.scaled() on every frame, but use scaled content to resize the image. This seems to ignore ratio, explaining why we need a new option flag.
2022-12-06 12:06:35 +01:00
519dd47ac6 chore(version): update version 2022-12-05 22:15:03 +01:00
0565a99639 chore(version): fixing all CI tests (#76)
* fix(ci): add Ubuntu dep and change pip install

* fix(ci): typo

* fix(ci): add missing deps for Ubuntu

* fix(ci): typo in pkg name

* fix(ci): trying to install manimpango

* fix(ci): append bin to path on MacOS

* fix(ci): install Python setup tool

* try(ci): trying some stuff

* fix(ci): remove useless pkgs

* fix(ci): check manimpango and remove pyopengl

* try(ci): try fix

* try(ci): cleaner workflow

* fix(ci): missing dollar

* try(ci): minimal platform

* try(ci): test

* fix(ci): reset offscreen option

* fix(ci): add opengl dep

* fix(ci): xvfb for pyglet

* fix(ci): correctly set env var

* try(ci): set DISPLAY env var at the beginning

* test(ci): switch minimal to offscreen

* test(ci): remove QT debug env var

* try(ci): fixing manimgl issue

* Revert "try(ci): fixing manimgl issue"

This reverts commit f76c05897013bd804810c7153953bde4c8715af7.

* try(ci): manimgl to manim-render

* try(ci): verbose

* fix(ci): update linux deps

* fix(ci): typo

* fix(ci): typo in deps

* fix(ci): typo

* fix(ci): test other deps

* test(ci): install manimpango

* try(ci): test

* test(ci): print help

* test(ci): reset verbose mode

* fix(ci): typo

* chore(setup): use poetry

* chore(setup): cache installs

* fix(ci): swap order

* fix(ci): poetry install

* chore(setup): add manim/manimgl to dev-deps

* try(ci): some test

* try(ci): import two maybe conflicting packages

* fix(ci): typo in cmd

* fix(ci): only check if manimgl

* fix(ci): remove useless check
2022-12-05 22:13:44 +01:00
8dfe600656 chore(version): release 4.7.0 2022-12-03 16:09:09 +01:00
03107867ab chore(deps): drop support for Python 3.7 (#84)
* chore(deps): drop support for Python 3.7

In link with the recent release of [Manim v0.17.0](https://github.com/ManimCommunity/manim/releases/tag/v0.17.0), Manim Slides drops support for Python 3.7.

* feat(cli): check for newer Manim Slides version

Uses same logic as manim to check for a new version

* fix(lib): reset correct version

* chore(setup): remove 3.7 classifier
2022-12-03 14:51:41 +01:00
bf64962c46 feat(cli): check for newer Manim Slides version (#85)
* chore(deps): drop support for Python 3.7

In link with the recent release of [Manim v0.17.0](https://github.com/ManimCommunity/manim/releases/tag/v0.17.0), Manim Slides drops support for Python 3.7.

* feat(cli): check for newer Manim Slides version

Uses same logic as manim to check for a new version

* fix(lib): reset correct version

* fix(ci): add type stubs for requests
2022-12-03 14:51:00 +01:00
97e7bf8cb0 chore(speed): avoid unnecessary color conversion (#83)
* chore(speed): avoid unnecessary color conversion

This speeds up a bit the presentation by avoiding color conversion from BGR (OpenCV) to RGB.

* [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-12-02 18:02:54 +01:00
1bca2683e1 refactor(lib): rename main to __main__ (#82)
* refactor(lib): rename `main` to `__main__`

This allows to call the module using `python -m manim-slides`, useful for profiling, etc.

* fix(setup): update name
2022-12-02 17:24:57 +01:00
d6bb82261c chore(setup): move setup config to pyproject.toml (#78)
* chore(setup): move setup config to pyproject.toml

* fix(ci): modify build steps

* fix(ci): remove / exclude useless stuff

* fix(ci): exclude does not work
2022-11-29 10:57:25 +01:00
0c682e4ec9 [pre-commit.ci] pre-commit autoupdate (#77)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-29 08:54:41 +01:00
2f0453c9a6 chore(lib): add missing type hints and mypy is happy! (#75)
* chore(lib): add missing type hints and mypy is happy!

Closes #34

* fix(ci): add missing dep for mypy
2022-11-28 14:28:42 +01:00
85ea9f3096 Fix flake8 check errors: unused imports, tab warning (#68)
* fix: unused imports, hide tab warning for the str

* refactor(defaults.py): move revealjs_template to separate file

* fix(lib): move data files and use pkg_resources

* fix(lib): remove unused and unexisting import

* fix(ci): only test conversion with Manim

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

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

* fix(ci): test ManimGL on Python 3.10, not 3.11

* fix(lib): include package data in setup.py

* fix(ci): no fail-fast

* fix(ci): typo

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-28 11:39:34 +01:00
1ae8db7966 chore(python): support and tests for Python 3.11 (#74)
Closes #71
2022-11-28 10:26:13 +01:00
82cccc3fc2 [pre-commit.ci] pre-commit autoupdate (#73)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-23 12:43:35 +01:00
726b0abf5a docs: correct virtual environment information (#72)
* docs: correct virtual environment information

* fix: format

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-11-18 07:29:24 +01:00
80f4f4e3f7 Remove redundant black hook & specify minimal supported python version for hooks (#70)
* fix(ci): remove duplicated hook

* feat(ci): specify minimal supported python version - hooks
2022-11-16 23:48:12 +01:00
82eebae686 fix(ci): setup minimal python version for flake8 (#69) 2022-11-16 10:07:37 +01:00
7367cc2cb5 [pre-commit.ci] pre-commit autoupdate (#67)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-15 08:07:14 +01:00
f26541eb32 chore(version): update version 2022-11-09 16:14:34 +01:00
06890ceacd fix(ci): typo in keyword args 2022-11-09 16:06:50 +01:00
9aa715a0e4 feat(cli): add convert option to generate html presentations (#66)
* wip(cli): convert slides to html using RevealJS

* wip: convert - almost fully working

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

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

* fix: remove unused file

* fix: add last slides in now performed during rendering

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

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

* chore(ci): testing ConvertExample too

* fix: ManimGL does not consider wait as an animation

* [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-11-09 15:59:19 +01:00
a373bdb460 feat(cli): add app icon (#64)
* feat: add icon

* feat: add app icon

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

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

* fix(lint): ignore resources file with mypy

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-02 10:47:37 +01:00
e3e79617c0 feat(cli): read environ variable for verbosity (#63)
* feat(cli): read environ variable for verbosity

* [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-10-31 21:48:17 +01:00
668de2c023 fix(ci): try fix 2022-10-31 16:22:58 +01:00
929caec018 fix(ci): another try 2022-10-31 15:59:39 +01:00
48cc3343bd fix(ci): try installing opengl 2022-10-31 15:54:06 +01:00
144e7dac5b fix(ci): trying another solution 2022-10-31 15:37:06 +01:00
8b56f42183 fix(ci): trying to fix build problem 2022-10-31 15:33:34 +01:00
534bc21672 fix(ci): remove impossible caching 2022-10-31 15:26:39 +01:00
b1a8768963 fix(ci): fixing workflows and caching 2022-10-31 15:21:58 +01:00
422e355758 chore(version): update version and change pyside version dep 2022-10-31 15:07:33 +01:00
3eb9fa0b74 refactor(lib): change PyQT5 to PySide6 (#62)
* refactor(lib): change PyQT5 to PySide6

This, hopefully, should now add support for M1 chips

* chore: update README and change imports
2022-10-31 14:55:03 +01:00
8f519ed134 Create FUNDING.yml 2022-10-31 10:11:32 +01:00
916e2aa2ab chore(version): update version 2022-10-31 09:15:01 +01:00
4d5f664348 chore(cli): change metavar for some options (#61)
This changes some metavars such that `--help` output is closer to the Sphinx documentation.
2022-10-31 09:13:32 +01:00
cb6a5bb35f feat: add option for background color (#60)
* feat: add option for background color

This allows to define the background color used for border when resize mode is set to "keep".

Closes #52

* [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-10-31 09:01:14 +01:00
bba05cce16 chore(version): update version 2022-10-24 09:27:14 +02:00
ad02c8296b feat(cli): add resize mode option (#59)
This adds a new option that allows to control how the video is going to be scaled. By default, we now use a smooth rescaling, but fast mode with no interpolation can be used (previously default).
2022-10-24 09:26:48 +02:00
0778cebef7 chore(README): update key bindings documentation 2022-10-24 09:20:14 +02:00
163260415b Mouse Show/Hide feature (#56)
* Mouse Show/Hide feature

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

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

* Mouse Hide/Show Fix

Applied requested changes for PR

* [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>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-10-21 12:57:53 +02:00
241419a781 Support template (#57)
* PATCH: Docs template enhacement

* FEAT: Template drafted for Support

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

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

* Update .github/ISSUE_TEMPLATE/documentation.yml

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Update .github/ISSUE_TEMPLATE/support.yml

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

Co-authored-by: Harisai <Harisaipravin_SV@intuit.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-10-21 10:55:39 +02:00
bac21815b2 chore(README): add note for M1 chips and doc. feat 2022-10-20 08:55:00 +02:00
2f8f7561a6 Issue more templates (#54)
* FEAT: Git template for documentation

* PATCH: Space keys updated

* PATCH: Spaccing update

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

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

* UPDATE: Code of conduct removed

* PATCH: Retry CI

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

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

Co-authored-by: Harisai <Harisaipravin_SV@intuit.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-19 18:27:41 +02:00
a489dfd0e8 feat: set application name 2022-10-19 14:32:16 +02:00
76ef16d98b fix(docs): typo 2022-10-19 11:51:43 +02:00
88125bf1ae Update __version__.py 2022-10-19 11:09:23 +02:00
cffc4ebbc5 Update __version__.py 2022-10-19 11:09:14 +02:00
d717bc651d refactor: use PyQT5 for window display (#49)
* wip: use PyQT5 for window display

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

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

* wip: first slide is shown

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

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

* wip: pushing non-working code

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

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

* wip: some logging

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

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

* feat: new configuration wizard working

* fix: prevent key error

* wip: making action work

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

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

* wip: soon done! info + video

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

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

* fix: bugs in sleep and exiting

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

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

* try: offscreen

* fix: pop default value if not present

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

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

* feat: add aspect ratio option

* chore: typing wip

* fix: now() function returns seconds, not milliseconds anymore

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-19 11:08:41 +02:00
bc3d55fce2 feat(cli): add basic logging facilities (#50)
* feat(cli): add basic logging facilities

This PR adds basic logging support, to be extended in the future.

* fix: typo in decorator name
2022-10-17 10:52:42 +02:00
5b9cb1523c chore(ci): pre-commit mypy (#41)
* chore(ci): pre-commit mypy

* chore: ignore non-lib files

* chore: ignore setup.py

* [pre-commit.ci] pre-commit autoupdate (#47)

updates:
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* chore(ci): pre-commit mypy

* chore: ignore non-lib files

* chore: ignore setup.py

* fix: update pre-commit config for mypy

* feat: add some missing type hints

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-11 11:05:58 +02:00
51a87840ce [pre-commit.ci] pre-commit autoupdate (#47)
updates:
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-11 10:10:30 +02:00
42550e8b29 Update defaults.py (#46)
fix the key id for macos
2022-10-10 07:31:52 +02:00
501813483c Added Pull Request Template (#43)
* Create feature_request.yml

* Issue template

* Create bug.yml

* Update bug.yml

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

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

* Update bug.yml

* Update bug.yml

* Created Pull Request Template

* Changed directory

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

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

* Update .github/pull_request_template.md

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-10-06 18:07:12 +02:00
0ae99c0f4d fix: typo in issue template 2022-10-06 09:17:07 +02:00
c2315928bd Created Bug Template (#40)
* Create feature_request.yml

* Issue template

* Create bug.yml

* Update bug.yml

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

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

* Update bug.yml

* Update bug.yml

* [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-10-05 19:31:31 +02:00
f3c8f3cc24 Add Feature Request Template (#39)
* Create ISSUE_TEMPLATE

* Update feature_request.yml

* [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-10-05 17:21:48 +02:00
14a266b139 Update README.md (#33)
Fixed small spelling mistake.
2022-10-05 10:53:34 +02:00
b697442fc0 chore(ci): add more style checks (#32)
* chore(ci): add more style checks

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

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

* chore(ci): remove vulture as flake8 replaces it

* chore(lib): add __all__ list

* chore(ci): make flake8 happy and remove mypy

* fix: config_path is actually used in commons

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-05 10:41:08 +02:00
4f8fae75cf [pre-commit.ci] pre-commit autoupdate (#31)
updates:
- [github.com/jendrikseipp/vulture: v2.3 → v2.6](https://github.com/jendrikseipp/vulture/compare/v2.3...v2.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-28 07:55:24 +02:00
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
e7c2bcfe98 Merge pull request #5 from jeertmans/reserve-video-and-more
feat(cli): reverse videos on the fly!
2022-09-08 00:07:27 +02:00
ac486f4f23 trying to fix builds 2022-09-08 00:03:47 +02:00
0bd29202e7 Revert "remove builds"
This reverts commit 78941e10fcd1e612333f4aab51faf6f3dc4a63b6.
2022-09-08 00:02:41 +02:00
061706922b Revert "remove dist"
This reverts commit dab96c980d1e9f1f2fb6f7d98b975330fa7841f5.
2022-09-08 00:02:35 +02:00
574c545f86 Merge pull request #4 from jeertmans/reserve-video-and-more
feat(cli): reverse videos on the fly!
2022-09-07 23:49:26 +02:00
dab96c980d remove dist 2022-09-07 23:48:49 +02:00
78941e10fc remove builds 2022-09-07 23:48:16 +02:00
255c86ba25 fix: manual install 2022-09-07 23:45:20 +02:00
981b34faf5 Merge remote-tracking branch 'origin/reserve-video-and-more' into reserve-video-and-more 2022-09-07 23:44:13 +02:00
cc42a1bac5 fix: CI 2022-09-07 23:44:08 +02:00
efa6fe05cf [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-09-07 21:40:31 +00:00
11494209e4 fix: import errors... 2022-09-07 23:40:05 +02:00
e0669707e7 Merge pull request #3 from jeertmans/reserve-video-and-more
feat(cli): reverse videos on the fly!
2022-09-07 23:36:03 +02:00
51ca828920 fix: typo 2022-09-07 23:35:40 +02:00
2076d65944 fix(deps): fix deps import error 2022-09-07 23:34:33 +02:00
9640605ae9 feat(cli): reverse videos on the fly 2022-09-07 23:24:54 +02:00
efbe488660 WIP: reversing videos, etc. 2022-09-07 20:04:07 +02:00
e82ab99186 Merge pull request #2 from jeertmans/windows-fix
fix(windows): enhance resolution in fullscreen mode
2022-09-07 20:03:04 +02:00
7128c01ab5 fix(hooks): reset black fmt 2022-08-26 08:55:22 +02:00
c6d44b0b3b fix(cli): add '.' after command description 2022-08-26 08:54:18 +02:00
14b334fa6f fix(windows): enhance resolution in fullscreen mode 2022-08-02 16:59:42 +02:00
68858c3c33 Merge pull request #1 from jeertmans/ThreeDSlide
Add ThreeDSlides support
2022-08-02 16:20:28 +02:00
3520c42d7c WIP: reversing videos, etc. 2022-07-18 12:38:06 +02:00
1720a7d742 Working 3DScene example 2022-07-13 23:04:12 +02:00
d9eab15fa5 WIP: ThreeDSlide 2022-07-13 18:00:11 +02:00
b3210ec285 fix: version bug & trick 2022-07-13 17:08:06 +02:00
db5fa33e0c fix: not including version in package :') 2022-07-12 17:32:13 +02:00
da0972ef43 Running pre-commit 2022-07-12 17:28:14 +02:00
8081617e29 Trick to not import manim 2022-07-12 17:25:28 +02:00
9247568531 Add badges and capitalize name 2022-07-12 17:16:11 +02:00
94e399ba91 Some files to ignore 2022-07-12 17:12:30 +02:00
b3f988421a Renaming LICENSE file 2022-07-12 17:12:13 +02:00
b9eb2b395a (WIP): Add 3D Scene example 2022-07-12 17:11:48 +02:00
f3d2c4e731 Refactor old work 2022-07-12 17:11:32 +02:00
ddeb20646d Update setup file with personal info & deps 2022-07-12 17:10:54 +02:00
588c4b285f Update README 2022-07-12 17:10:38 +02:00
6fdf6d670b Add pre-commit CI 2022-07-12 17:10:06 +02:00
2a8b6734ec Refactor publishing CI 2022-07-12 17:09:55 +02:00
d2f04341fa Add gif of example 2022-07-12 17:09:26 +02:00
19fa17212c Bump version to 0.2.1 2021-06-28 20:20:08 +02:00
73107f5065 Updated readme 2021-06-28 20:19:36 +02:00
0429cf3543 Added manim-presentation-wizard and manim-presentation commands 2021-06-28 20:17:23 +02:00
87bbc9a8c6 Added configuration wizard 2021-06-28 20:16:46 +02:00
c0a5a70f93 Merge branch 'glatteis-main' 2021-06-28 19:47:35 +02:00
ea171e1301 Fix memory problems 2021-06-27 13:53:06 +02:00
d1c3e9c075 major improvements, bumping to 2.0 2021-06-18 21:43:08 +02:00
38e03db9e9 updated readme 2021-06-18 21:33:11 +02:00
2eafa0b82e Solves #4 2021-06-18 21:31:10 +02:00
8a4fea687d added show next frame as last frame hack (#7) 2021-06-18 21:26:46 +02:00
128d6718ae automatically got to next Scene 2021-06-18 21:19:16 +02:00
22cbb7ec94 Merge branch 'glatteis-main' 2021-06-18 20:10:12 +02:00
5feb13da10 updated readme 2021-06-18 20:08:50 +02:00
90b2e4d46b modified example 2021-06-18 20:06:31 +02:00
43b9fa7cf7 removed leftover self.play 2021-06-18 20:05:27 +02:00
1cc070db86 Re added add_last_slide becouse it was broken if the last slide was a loop 2021-06-18 20:02:24 +02:00
c5274fb57f pause() displays final frame not pre-final 2021-06-18 14:43:06 +02:00
2a136ed585 Fix pause, loops 2021-06-11 09:10:02 +02:00
c9390f0e59 Fix loops 2021-06-11 08:42:56 +02:00
41de205675 Add fullscreen option 2021-06-05 14:25:29 +02:00
133ec17ebb Fix a lot of stuff in present.py 2021-06-05 14:16:17 +02:00
09199777e0 Support for windows keys and config file as in #4 2021-06-05 13:36:54 +02:00
bfcf7db26e bump to 0.1.5 2021-05-22 12:14:54 +02:00
b6522f4756 bump version to 0.1.4 2021-05-22 12:10:05 +02:00
4216299b39 Fix #3 2021-05-22 12:09:07 +02:00
cc8df92596 bump version 2021-04-24 16:18:16 +02:00
ef22141f91 safe current_slide_i increment 2021-04-24 16:16:11 +02:00
6881e24954 fix readme typo 2021-04-24 15:50:23 +02:00
d7d6ac610a readme update 2021-04-24 15:44:38 +02:00
49 changed files with 6548 additions and 443 deletions

5
.flake8 Normal file
View File

@ -0,0 +1,5 @@
[flake8]
min_python_version = 3.7
extend-ignore =
# E501: line too long
E501,

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [jeertmans]

40
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Bug
description: Report an issue to help improve the project.
labels: "bug"
title: '[BUG] <description>'
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
validations:
required: true
- type: textarea
id: platform
attributes:
label: Platform
description: What is your platform. Linux, macOS, or Windows?
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this bug?
validations:
required: false

View File

@ -0,0 +1,59 @@
name: Documentation
description: Ask / Report an issue related to the documentation.
title: "DOC: <description>"
labels: ['bug', 'docs']
body:
- type: markdown
attributes:
value: >
**Thank you for wanting to report a problem with manim-slides docs!**
If the problem seems straightforward, feel free to submit a PR instead!
Verify first that your issue is not already reported on GitHub [Issues].
[Issues]:
https://github.com/jeertmans/manim-slides/issues
- type: textarea
attributes:
label: Describe the Issue
description: A clear and concise description of the issue you encountered.
validations:
required: true
- type: input
attributes:
label: Affected Page
description: Add a link to page with the problem.
validations:
required: true
- type: dropdown
attributes:
label: Issue Type
description: >
Please select the option in the drop-down.
<details>
<summary>
<em>Issue?</em>
</summary>
</details>
options:
- Documentation Enhancement
- Documentation Report
validations:
required: true
- type: textarea
attributes:
label: Recommended fix or suggestions
description: A clear and concise description of how you want to update it.
validations:
required: false

View File

@ -0,0 +1,26 @@
name: Feature Request
description: Have a new idea/feature? Please suggest!
labels: "enhancement"
title: '[FEATURE] <description>'
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the enhancement you propose, also include what you tried and what worked.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this idea?
validations:
required: false

14
.github/ISSUE_TEMPLATE/support.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Question/Help/Support
description: Ask us about Manim Slides
title: "Support: Ask us anything"
labels: ['help', 'question']
body:
- type: textarea
attributes:
label: "Please explain the issue you're experiencing (with as much detail as possible):"
description: >
Please make sure to leave a reference to the document/code you're
referring to.
validations:
required: true

26
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,26 @@
<!-- If your PR fixes an open issue, use `Closes #999` to link your PR with the issue. #999 stands for the issue number you are fixing -->
## Fixes Issue
<!-- Remove this section if not applicable -->
<!-- Example: Closes #31 -->
## Description
<!-- Describe all the proposed changes in your PR -->
## Check List (Check all the applicable boxes)
- [ ] I understand that my contributions needs to pass the checks.
- [ ] If I created new functions / methods, I documented them and add type hints.
- [ ] If I modified already existing code, I updated the documentation accordingly.
- [ ] The title of my pull request is a short description of the requested changes.
## Screenshots
<!-- Add all the screenshots which support your changes -->
## Note to reviewers
<!-- Add notes to reviewers if applicable -->

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

View File

@ -1,22 +0,0 @@
name: Publish top PyPI
on: push
jobs:
build-n-publish:
name: Build and publish to PyPI
if: startsWith(github.ref, 'refs/tags')
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install
run: python -m pip install build --user
- name: Build binary wheel and a source tarball
run: python -m build --sdist --wheel --outdir dist/ .
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}

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

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

@ -0,0 +1,61 @@
# 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: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
cache: 'poetry'
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Install Linux Dependencies
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install Python dependencies
run: pip install manim sphinx sphinx_click furo
- name: Install local Python package
run: poetry install --with docs
- name: Build animation and convert it into HTML slides
run: |
poetry run manim example.py ConvertExample
poetry run manim-slides convert ConvertExample docs/source/_static/slides.html -ccontrols=true
- 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

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

@ -0,0 +1,48 @@
# Modified from: https://github.com/pypa/cibuildwheel
name: Upload Python Package
on:
push:
release:
types: [published]
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Install build package
run: python -m pip install -U build
- name: Build wheels
run: python -m build --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 }}

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

@ -0,0 +1,117 @@
on:
pull_request:
paths:
- '**.py'
- '.github/workflows/test_examples.yml'
workflow_dispatch:
name: Test Examples
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: ":99"
jobs:
build-examples:
strategy:
fail-fast: false
matrix:
manim: [manim, manimgl]
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.8', '3.9', '3.10', '3.11']
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
# We only test Python 3.11 on Windows and MacOS
- os: windows-latest
pyversion: '3.8'
- os: windows-latest
pyversion: '3.9'
- os: windows-latest
pyversion: '3.10'
manim: manim
- os: macos-latest
pyversion: '3.8'
- os: macos-latest
pyversion: '3.9'
- os: macos-latest
pyversion: '3.10'
manim: manim
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
cache: 'poetry'
# Path related stuff
- name: Append to Path on MacOS
if: matrix.os == 'macos-latest'
run: |
echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
- name: Append to Path on Ubuntu
if: 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
# OS depedencies
- name: Install manim dependencies on MacOs
if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
run: brew install ffmpeg py3cairo
- name: Install manimgl dependencies on MacOS
if: matrix.os == 'macos-latest' && matrix.manim == 'manimgl'
run: brew install ffmpeg
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install manimgl dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: |
sudo apt-get install libpango1.0-dev ffmpeg freeglut3-dev
- name: Install xvfb on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: |
sudo apt-get install xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
# Install Manim Slides
- name: Install Manim Slides
run: |
poetry config experimental.new-installer false
poetry install --with test
# Render slides
- name: Render slides
if: matrix.manim == 'manim'
run: poetry run manim -ql example.py Example ThreeDExample
- name: Render slides
if: matrix.manim == 'manimgl'
run: poetry run -v manimgl -l example.py Example ThreeDExample
# Play slides
- name: Test slides
run: poetry run manim-slides Example ThreeDExample --skip-all
# Test slides to html
- name: Test convert on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: |
poetry run manim -ql example.py ConvertExample
poetry run manim-slides convert --to=html ConvertExample index.html

20
.gitignore vendored
View File

@ -8,4 +8,22 @@ __pycache__/
/media
/presentation
/.vscode
/.vscode
slides/
.manim-slides.json
videos/
images/
docs/build/
docs/source/_static/slides_assets/
docs/source/_static/slides.html
slides_assets/
slides.html

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

@ -0,0 +1,59 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.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: ["--python-version", "37", "--profile", "black"]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
args: ["--target-version", "py37"]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- flake8-comprehensions
- flake8-tidy-imports
- flake8-typing-imports
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.991'
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
args:
- --install-types
- --non-interactive
- --ignore-missing-imports
# Disallow dynamic typing
- --disallow-any-generics
- --disallow-subclassing-any
# Disallow untyped definitions and calls
- --disallow-untyped-defs
- --disallow-incomplete-defs
- --check-untyped-defs
# None and optional handling
- --no-implicit-optional
# Configuring warnings
- --warn-unused-ignores
- --warn-no-return
- --no-warn-return-any
- --warn-redundant-casts
# Strict equality
- --strict-equality
# Config file
- --warn-unused-configs

View File

@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

199
README.md
View File

@ -1,32 +1,78 @@
# manim-presentation
![Manim Slides Logo](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png)
Tool for live presentations using [manim](https://www.manim.community/)
[![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
## Install
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 extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
- [Installation](#installation)
* [Dependencies](#dependencies)
* [Pip install](#pip-install)
* [Install From Repository](#install-from-repository)
- [Usage](#usage)
* [Basic Example](#basic-example)
* [Key Bindings](#key-bindings)
* [Interactive Tutorial](#interactive-tutorial)
* [Other Examples](#other-examples)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [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 virtual environment (e.g., [venv](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 specific 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
```
pip install manim_presentation opencv-python
### Install From Repository
An alternative way to install Manim Slides is to clone the git repository, and install from there:
```bash
git clone https://github.com/jeertmans/manim-slides
pip install -e .
```
> *Note:* the `-e` flag allows you to edit the files, and observe the changes directly when using Manim Slides
## Usage
Use the class `Slide` as your scenes base class
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
from manim_presentation import Slide
# example.py
class Example(Slide):
def construct(self):
...
```
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings)
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue)
```python
from manim import *
from manim_presentation import Slide
# or: from manimlib import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
@ -34,72 +80,107 @@ 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()
```
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_presentation Scene1 Scene2...
```bash
manim-slides [OPTIONS] Scene1 Scene2...
```
## Default Keybindings
Or in this example:
Default keybindings to control the presentation
| Keybinding | Action |
|:-----------:|:------------------------:|
| Right Arrow | Continue/Next Slide |
| Left Arrow | Previous Slide |
| R | Re-Animate Current Slide |
| Spacebar | Play/Pause |
| Q | Quit |
## Run Example
Clone this repository
```
git clone https://github.com/galatolofederico/manim-presentation.git
cd manim-presentation
```bash
manim-slides Example
```
Create a virtualenv
## Key Bindings
```
virtualenv --python=python3.7 env
. ./env/bin/activate
The default key bindings to control the presentation are:
![manim-wizard](https://user-images.githubusercontent.com/27275099/197468787-19c83a81-d757-47b9-8f68-218427d30298.png)
You can run the **configuration wizard** to change those key bindings:
```bash
manim-slides wizard
```
Install `manim` and `manim_presentation`
A default file can be created with:
```
pip install manim manim_presentation opencv-python
```bash
manim-slides init
```
Render the example scene
> **_NOTE:_** `manim-slides` uses key codes, which are platform dependent. Using the configuration wizard is therefore highly recommended.
```
manim -qh example.py
```
## Interactive Tutorial
Run the presentation
Click on the image to watch a slides presentation that explains you how to use Manim Slides.
```
manim_presentation Example
```
[![Manim Slides Docs](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/docs.png)](https://eertmans.be/manim-slides/)
## Contributions and license
## Other Examples
The code is released as Free Software under the [GNU/GPLv3](https://choosealicense.com/licenses/gpl-3.0/) license. Copying, adapting e republishing it is not only consent but also encouraged.
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.
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)
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 Similar Tools
There exists are variety of tools that allows to create slides presentations containing Manim animations.
Below is a comparison of the most used ones with Manim Slides:
| Project name | Manim Slides | Manim Presentation | Manim Editor | Jupyter Notebooks |
|:------------:|:------------:|:------------------:|:------------:|:-----------------:|
| Link | [![GitHub Repo stars](https://img.shields.io/github/stars/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/stars/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/stars/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/stars/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Activity | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Usage | Command-line | Command-line | Web Browser | Notebook |
| Note | Requires minimal modif. in scenes files | Requires minimal modif. in scenes files | Requires the usage of sections, and configuration through graphical interface | Relies on `nbconvert` to create slides from a Notebook |
| Support for ManimGL | Yes | No | No | No |
| Web Browser presentations | Yes | No | Yes | No |
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
## F.A.Q
### 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/
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/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

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

@ -0,0 +1,27 @@
# type: ignore
# 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"]

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

@ -0,0 +1,28 @@
.. 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: 600px
:align: center
:alt: Manim Slide logo
Welcome to Manim Slide's CLI documentation!
===========================================
.. raw:: html
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="_static/slides.html"></iframe></div>
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,5 +1,20 @@
from manim import *
from manim_presentation import Slide
# flake8: noqa: F403, F405
# type: ignore
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
class Example(Slide):
def construct(self):
@ -16,4 +31,314 @@ class Example(Slide):
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.wait()
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 ConvertExample(Slide):
"""WARNING: this example does not seem to work with ManimGL."""
def tinywait(self):
self.wait(0.1)
def construct(self):
title = VGroup(
Text("From Manim animations", t2c={"From": BLUE}),
Text("to slides presentation", t2c={"to": BLUE}),
Text("with Manim Slides", t2w={"[-12:]": BOLD}, t2c={"[-13:]": YELLOW}),
).arrange(DOWN)
step_1 = Text("1. In your scenes file, import Manim Slides")
step_2 = Text("2. Replace Scene with Slide")
step_3 = Text("3. In construct, add pauses where you need")
step_4 = Text("4. You can also create loops")
step_5 = Text("5. Render you scene with Manim")
step_6 = Text("6. Open your presentation with Manim Slides")
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
step.scale(0.5).to_corner(UL)
step = step_1
self.play(FadeIn(title))
self.pause()
code = Code(
code="""from manim import *
class Example(Scene):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
square = Square()
self.play(Transform(dot, square))
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_1 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Scene):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
square = Square()
self.play(Transform(dot, square))
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_2 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
square = Square()
self.play(Transform(dot, square))
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_3 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
self.pause()
square = Square()
self.play(Transform(dot, square))
self.pause()
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_4 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot()
self.add(dot)
self.start_loop()
self.play(Indicate(dot, scale_factor=2))
self.end_loop()
square = Square()
self.play(Transform(dot, square))
self.pause()
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_5 = Code(
code="manim example.py Example",
language="console",
)
code_step_6 = Code(
code="manim-slides Example",
language="console",
)
or_text = Text("or generate HTML presentation").scale(0.5)
code_step_7 = Code(
code="manim-slides convert Example slides.html --open",
language="console",
).shift(DOWN)
self.clear()
self.play(FadeIn(code))
self.tinywait()
self.pause()
self.play(FadeIn(step, shift=RIGHT))
self.play(Transform(code, code_step_1))
self.tinywait()
self.pause()
self.play(Transform(step, step_2))
self.play(Transform(code, code_step_2))
self.tinywait()
self.pause()
self.play(Transform(step, step_3))
self.play(Transform(code, code_step_3))
self.tinywait()
self.pause()
self.play(Transform(step, step_4))
self.play(Transform(code, code_step_4))
self.tinywait()
self.pause()
self.play(Transform(step, step_5))
self.play(Transform(code, code_step_5))
self.tinywait()
self.pause()
self.play(Transform(step, step_6))
self.play(Transform(code, code_step_6))
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
self.tinywait()
self.pause()
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
self.start_loop()
self.play(FadeIn(watch_text))
self.play(FadeOut(watch_text))
self.end_loop()
self.clear()
dot = Dot()
self.add(dot)
self.start_loop()
self.play(Indicate(dot, scale_factor=2))
self.end_loop()
square = Square()
self.play(Transform(dot, square))
self.remove(dot)
self.add(square)
self.tinywait()
self.pause()
self.play(Rotate(square, angle=PI / 4))
self.tinywait()
self.pause()
learn_more_text = (
VGroup(
Text("Learn more about Manim Slides:"),
Text("https://github.com/jeertmans/manim-slides", color=YELLOW),
)
.arrange(DOWN)
.scale(0.75)
)
self.play(Transform(square, learn_more_text))
self.tinywait()
# For ThreeDExample, things are different
if not MANIMGL:
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.add(axes)
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))
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))

6
manim-slides.qrc Normal file
View File

@ -0,0 +1,6 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file alias="icon.png">static/icon.png</file>
</qresource>
</RCC>

View File

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

View File

@ -1,246 +0,0 @@
import cv2
import numpy as np
import os
import sys
import json
import math
import time
import argparse
from enum import Enum
class Config:
QUIT_KEY = ord("q")
CONTINUE_KEY = 83 #right arrow
BACK_KEY = 81 #left arrow
REWIND_KEY = ord("r")
PLAYPAUSE_KEY = 32 #spacebar
class State(Enum):
PLAYING = 0
PAUSED = 1
WAIT = 2
END = 3
def __str__(self):
if self.value == 0: return "Playing"
if self.value == 1: return "Paused"
if self.value == 2: return "Wait"
if self.value == 3: return "End"
return "..."
def now():
return round(time.time() * 1000)
def fix_time(x):
return x if x > 0 else 1
class Presentation:
def __init__(self, config):
self.slides = config["slides"]
self.files = config["files"]
self.reset()
self.load_files()
self.add_last_slide()
def reset(self):
self.current_animation = 0
self.current_slide_i = 0
def add_last_slide(self):
last_slide_end = self.slides[-1]["end_animation"]
last_animation = len(self.files) - 1
self.slides.append(dict(
start_animation = last_slide_end,
end_animation = last_animation,
type = "last",
number = len(self.slides) + 1
))
def load_files(self):
self.caps = list()
for f in self.files:
self.caps.append(cv2.VideoCapture(f))
def next(self):
self.current_slide_i += 1
self.current_animation = self.current_slide["start_animation"]
def prev(self):
self.current_slide_i = max(0, self.current_slide_i - 1)
self.current_animation = self.current_slide["start_animation"]
def rewind(self):
self.current_animation = self.current_slide["start_animation"]
@property
def current_slide(self):
return self.slides[self.current_slide_i]
@property
def current_cap(self):
return self.caps[self.current_animation]
@property
def fps(self):
return self.current_cap.get(cv2.CAP_PROP_FPS)
def get_frame_and_state(self):
ret, frame = self.current_cap.read()
state = State.PLAYING
if ret:
self.lastframe = frame
else:
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
if self.current_slide["end_animation"] == self.current_animation + 1:
if self.current_slide["type"] == "slide":
state = State.WAIT
elif self.current_slide["type"] == "loop":
self.current_animation = self.current_slide["start_animation"]
elif self.current_slide["type"] == "last":
return self.lastframe, State.END
elif self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation:
return self.lastframe, State.END
else:
self.current_animation += 1
return self.lastframe, state
class Display:
def __init__(self, presentations, start_paused=False):
self.presentations = presentations
self.start_paused = start_paused
self.state = State.PLAYING
self.lastframe = None
self.current_presentation_i = 0
self.lag = 0
self.last_time = now()
@property
def current_presentation(self):
return self.presentations[self.current_presentation_i]
def run(self):
while True:
if self.state == State.PLAYING:
self.lastframe, self.state = self.current_presentation.get_frame_and_state()
if self.start_paused:
self.state = State.PAUSED
self.start_paused = False
if self.state == State.END:
if self.current_presentation_i == len(self.presentations) - 1:
self.quit()
else:
self.current_presentation_i += 1
self.state = State.PLAYING
self.show_video()
self.show_info()
self.handle_key()
def show_video(self):
self.lag = now() - self.last_time
self.last_time = now()
cv2.imshow("Video", self.lastframe)
def show_info(self):
info = np.zeros((130, 420), np.uint8)
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
grid_x = [30, 230]
grid_y = [30, 70, 110]
cv2.putText(
info,
f"Animation: {self.current_presentation.current_animation}",
(grid_x[0], grid_y[0]),
*font_args
)
cv2.putText(
info,
f"State: {self.state}",
(grid_x[1], grid_y[0]),
*font_args
)
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
(grid_x[0], grid_y[1]),
*font_args
)
cv2.putText(
info,
f"Slide Type: {self.current_presentation.current_slide['type']}",
(grid_x[1], grid_y[1]),
*font_args
)
cv2.putText(
info,
f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}",
((grid_x[0]+grid_x[1])//2, grid_y[2]),
*font_args
)
cv2.imshow("Info", info)
def handle_key(self):
sleep_time = math.ceil(1000/self.current_presentation.fps)
key = cv2.waitKey(fix_time(sleep_time - self.lag)) & 0xFF
if key == Config.QUIT_KEY:
self.quit()
elif self.state == State.PLAYING and key == Config.PLAYPAUSE_KEY:
self.state = State.PAUSED
elif self.state == State.PAUSED and key == Config.PLAYPAUSE_KEY:
self.state = State.PLAYING
elif self.state == State.WAIT and (key == Config.CONTINUE_KEY or key == Config.PLAYPAUSE_KEY):
self.current_presentation.next()
self.state = State.PLAYING
elif self.state == State.PLAYING and key == Config.CONTINUE_KEY:
self.current_presentation.next()
elif key == Config.BACK_KEY:
if self.current_presentation.current_slide_i == 0:
self.current_presentation_i = max(0, self.current_presentation_i - 1)
self.current_presentation.reset()
self.state = State.PLAYING
else:
self.current_presentation.prev()
self.state = State.PLAYING
elif key == Config.REWIND_KEY:
self.current_presentation.rewind()
self.state = State.PLAYING
def quit(self):
cv2.destroyAllWindows()
sys.exit()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("scenes", metavar="scenes", type=str, nargs="+", help="Scenes to present")
parser.add_argument("--folder", type=str, default="./presentation", help="Presentation files folder")
parser.add_argument("--start-paused", action="store_true", help="Start paused")
args = parser.parse_args()
presentations = list()
for scene in args.scenes:
config_file = os.path.join(args.folder, f"{scene}.json")
if not os.path.exists(config_file):
raise Exception(f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class")
config = json.load(open(config_file))
presentations.append(Presentation(config))
display = Display(presentations, start_paused=args.start_paused)
display.run()
if __name__ == "__main__":
main()

View File

@ -1,82 +0,0 @@
import os
import json
import shutil
from manim import Scene, config
class Slide(Scene):
def __init__(self, *args, **kwargs):
self.output_folder = kwargs.pop("output_folder", "./presentation")
super(Slide, self).__init__(*args, **kwargs)
self.slides = list()
self.current_slide = 1
self.current_animation = 0
self.loop_start_animation = None
self.pause_start_animation = 0
def play(self, *args, **kwargs):
super(Slide, self).play(*args, **kwargs)
self.current_animation += 1
def pause(self):
self.slides.append(dict(
type="slide",
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide
))
self.current_slide += 1
self.pause_start_animation = self.current_animation
def start_loop(self):
assert self.loop_start_animation is None, "You cant nest loops"
self.loop_start_animation = self.current_animation
def end_loop(self):
assert self.loop_start_animation is not None, "You have to start a loop before ending it"
self.slides.append(dict(
type="loop",
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide
))
self.current_slide += 1
self.loop_start_animation = None
self.pause_start_animation = self.current_animation
def render(self, *args, **kwargs):
# We need to disable the caching limit since we rely on intermidiate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
super(Slide, self).render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
files_folder = os.path.join(self.output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
scene_name = type(self).__name__
scene_files_folder = os.path.join(files_folder, scene_name)
if os.path.exists(scene_files_folder):
shutil.rmtree(scene_files_folder)
if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder)
files = list()
for src_file in self.renderer.file_writer.partial_movie_files:
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file))
shutil.copyfile(src_file, dst_file)
files.append(dst_file)
f = open(os.path.join(self.output_folder, "%s.json" % (scene_name, )), "w")
json.dump(dict(
slides=self.slides,
files=files
), f)
f.close()

3
manim_slides/__init__.py Normal file
View File

@ -0,0 +1,3 @@
# flake8: noqa: F401
from .__version__ import __version__
from .slide import Slide, ThreeDSlide

73
manim_slides/__main__.py Normal file
View File

@ -0,0 +1,73 @@
import json
import click
import requests
from click_default_group import DefaultGroup
from . import __version__
from .convert import convert
from .manim import logger
from .present import list_scenes, present
from .wizard import init, wizard
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
@click.option(
"--notify-outdated-version/--silent",
" /-S",
is_flag=True,
default=True,
help="Check if a new version of Manim Slides is available.",
)
@click.version_option(__version__, "-v", "--version")
@click.help_option("-h", "--help")
def cli(notify_outdated_version: bool) -> None:
"""
Manim Slides command-line utilities.
If no command is specified, defaults to `present`.
"""
# Code below is mostly a copy from:
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
if notify_outdated_version:
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
try:
req_info: requests.models.Response = requests.get(
manim_info_url, timeout=10
)
req_info.raise_for_status()
stable = req_info.json()["info"]["version"]
if stable != __version__:
click.echo(
"You are using Manim Slides version "
+ click.style(f"v{__version__}", fg="red")
+ ", but version "
+ click.style(f"v{stable}", fg="green")
+ " is available."
)
click.echo(
"You should consider upgrading via "
+ click.style("pip install -U manim-slides", fg="yellow")
)
except requests.exceptions.HTTPError:
logger.debug(f"HTTP Error: {warn_prompt}")
except requests.exceptions.ConnectionError:
logger.debug(f"Connection Error: {warn_prompt}")
except requests.exceptions.Timeout:
logger.debug(f"Timed Out: {warn_prompt}")
except json.JSONDecodeError:
logger.debug(warn_prompt)
logger.debug(f"Error decoding JSON from {manim_info_url}")
except Exception:
logger.debug(f"Something went wrong: {warn_prompt}")
cli.add_command(convert)
cli.add_command(init)
cli.add_command(list_scenes)
cli.add_command(present)
cli.add_command(wizard)
if __name__ == "__main__":
cli()

View File

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

82
manim_slides/commons.py Normal file
View File

@ -0,0 +1,82 @@
from typing import Any, Callable
import click
from click import Context, Parameter
from .defaults import CONFIG_PATH, FOLDER_PATH
from .manim import logger
F = Callable[..., Any]
Wrapper = Callable[[F], F]
def config_path_option(function: F) -> F:
"""Wraps a function to add configuration path option."""
wrapper: Wrapper = click.option(
"-c",
"--config",
"config_path",
metavar="FILE",
default=CONFIG_PATH,
type=click.Path(dir_okay=False),
help="Set path to configuration file.",
show_default=True,
)
return wrapper(function)
def config_options(function: F) -> F:
"""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."
)(function)
function = click.option(
"-m",
"--merge",
is_flag=True,
help="Merge any existing configuration file with the new configuration.",
)(function)
return function
def verbosity_option(function: F) -> F:
"""Wraps a function to add verbosity option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
logger.setLevel(value)
wrapper: Wrapper = click.option(
"-v",
"--verbosity",
type=click.Choice(
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
case_sensitive=False,
),
help="Verbosity of CLI output",
default=None,
expose_value=False,
envvar="MANIM_SLIDES_VERBOSITY",
show_envvar=True,
callback=callback,
)
return wrapper(function)
def folder_path_option(function: F) -> F:
"""Wraps a function to add folder path option."""
wrapper: Wrapper = click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
show_default=True,
)
return wrapper(function)

253
manim_slides/config.py Normal file
View File

@ -0,0 +1,253 @@
import os
import shutil
import subprocess
import tempfile
from enum import Enum
from typing import Callable, Dict, List, Optional, Set, Union
from pydantic import BaseModel, root_validator, validator
from PySide6.QtCore import Qt
from .manim import FFMPEG_BIN, logger
def merge_basenames(files: List[str]) -> str:
"""
Merge multiple filenames by concatenating basenames.
"""
dirname = os.path.dirname(files[0])
_, ext = os.path.splitext(files[0])
basename = "_".join(os.path.splitext(os.path.basename(file))[0] for file in files)
return os.path.join(dirname, basename + ext)
class Key(BaseModel): # type: ignore
"""Represents a list of key codes, with optionally a name."""
ids: Set[int]
name: Optional[str] = None
def set_ids(self, *ids: int) -> None:
self.ids = set(ids)
@validator("ids", each_item=True)
def id_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Key ids cannot be negative integers")
return v
def match(self, key_id: int) -> bool:
m = key_id in self.ids
if m:
logger.debug(f"Pressed key: {self.name}")
return m
class Config(BaseModel): # type: ignore
"""General Manim Slides config"""
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
CONTINUE: Key = Key(ids=[Qt.Key_Right], name="CONTINUE / NEXT")
BACK: Key = Key(ids=[Qt.Key_Left], name="BACK")
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
REWIND: Key = Key(ids=[Qt.Key_R], name="REWIND")
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
@root_validator
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
ids: Set[int] = set()
for key in values.values():
if len(ids.intersection(key.ids)) != 0:
raise ValueError(
"Two or more keys share a common key code: please make sure each key has distinct 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
class SlideType(str, Enum):
slide = "slide"
loop = "loop"
last = "last"
class SlideConfig(BaseModel): # type: ignore
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) -> 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) -> 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: Dict[str, Union[SlideType, int, bool]]
) -> Dict[str, Union[SlideType, int, bool]]:
if values["start_animation"] >= values["end_animation"]: # type: ignore
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. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
)
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return values
def is_slide(self) -> bool:
return self.type == SlideType.slide
def is_loop(self) -> bool:
return self.type == SlideType.loop
def is_last(self) -> bool:
return self.type == SlideType.last
@property
def slides_slice(self) -> slice:
return slice(self.start_animation, self.end_animation)
class PresentationConfig(BaseModel): # type: ignore
slides: List[SlideConfig]
files: List[str]
@validator("files", pre=True, each_item=True)
def is_file_and_exists(cls, v: str) -> str:
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: Dict[str, Union[List[SlideConfig], List[str]]]
) -> Dict[str, Union[List[SlideConfig], List[str]]]:
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: # type: ignore
raise ValueError(
f"The following slide's contains animations not listed in files {files}: {slide}"
)
return values
def move_to(self, dest: str, copy: bool = True) -> "PresentationConfig":
"""
Moves (or copy) the files to a given directory.
"""
copy_func: Callable[[str, str], None] = shutil.copy
move_func: Callable[[str, str], None] = shutil.move
move = copy_func if copy else move_func
n = len(self.files)
for i in range(n):
file = self.files[i]
basename = os.path.basename(file)
dest_path = os.path.join(dest, basename)
logger.debug(f"Moving / copying {file} to {dest_path}")
move(file, dest_path)
self.files[i] = dest_path
return self
def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
"""
Concatenate animations such that each slide contains one animation.
"""
dest_paths = []
for i, slide_config in enumerate(self.slides):
files = self.files[slide_config.slides_slice]
if len(files) > 1:
dest_path = merge_basenames(files)
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
f.writelines(f"file {os.path.abspath(path)}\n" for path in files)
f.close()
command = [
FFMPEG_BIN,
"-f",
"concat",
"-safe",
"0",
"-i",
f.name,
"-c",
"copy",
dest_path,
"-y",
]
logger.debug(" ".join(command))
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
output, error = process.communicate()
if output:
logger.debug(output.decode())
if error:
logger.debug(error.decode())
dest_paths.append(dest_path)
else:
dest_paths.append(files[0])
slide_config.start_animation = i
slide_config.end_animation = i + 1
self.files = dest_paths
if dest:
return self.move_to(dest)
return self
DEFAULT_CONFIG = Config()

467
manim_slides/convert.py Normal file
View File

@ -0,0 +1,467 @@
import os
import webbrowser
from enum import Enum
from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union
import click
import pkg_resources
from click import Context, Parameter
from pydantic import BaseModel, PositiveInt, ValidationError
from .commons import folder_path_option, verbosity_option
from .config import PresentationConfig
from .present import get_scenes_presentation_config
def validate_config_option(
ctx: Context, param: Parameter, value: Any
) -> Dict[str, str]:
config = {}
for c_option in value:
try:
key, value = c_option.split("=")
config[key] = value
except ValueError:
raise click.BadParameter(
f"Configuration options `{c_option}` could not be parsed into a proper (key, value) pair. Please use an `=` sign to separate key from value."
)
return config
class Converter(BaseModel): # type: ignore
presentation_configs: List[PresentationConfig] = []
assets_dir: str = "{basename}_assets"
template: Optional[str] = None
def convert_to(self, dest: str) -> None:
"""Converts self, i.e., a list of presentations, into a given format."""
raise NotImplementedError
def load_template(self) -> str:
"""Returns the template as a string.
An empty string is returned if no template is used."""
return ""
def open(self, file: str) -> bool:
"""Opens a file, generated with converter, using appropriate application."""
raise NotImplementedError
@classmethod
def from_string(cls, s: str) -> Type["Converter"]:
"""Returns the appropriate converter from a string name."""
return {
"html": RevealJS,
}[s]
class Str(str):
"""A simple string, but quoted when needed."""
# This fixes pickling issue on Python 3.8
__reduce_ex__ = str.__reduce_ex__
def __str__(self) -> str:
"""Ensures that the string is correctly quoted."""
if self in ["true", "false", "null"]:
return super().__str__()
else:
return f"'{super().__str__()}'"
Function = str # Basically, anything
class JsTrue(str, Enum):
true = "true"
class JsFalse(str, Enum):
false = "false"
class JsBool(Str, Enum): # type: ignore
true = "true"
false = "false"
class JsNull(Str, Enum): # type: ignore
null = "null"
class ControlsLayout(Str, Enum): # type: ignore
edges = "edges"
bottom_right = "bottom-right"
class ControlsBackArrows(Str, Enum): # type: ignore
faded = "faded"
hidden = "hidden"
visibly = "visibly"
class SlideNumber(Str, Enum): # type: ignore
true = "true"
false = "false"
hdotv = "h.v"
handv = "h/v"
c = "c"
candt = "c/t"
class ShowSlideNumber(Str, Enum): # type: ignore
all = "all"
print = "print"
speaker = "speaker"
class KeyboardCondition(Str, Enum): # type: ignore
null = "null"
focused = "focused"
class NavigationMode(Str, Enum): # type: ignore
default = "default"
linear = "linear"
grid = "grid"
class AutoPlayMedia(Str, Enum): # type: ignore
null = "null"
true = "true"
false = "false"
PreloadIframes = AutoPlayMedia
class AutoAnimateMatcher(Str, Enum): # type: ignore
null = "null"
class AutoAnimateEasing(Str, Enum): # type: ignore
ease = "ease"
AutoSlide = Union[PositiveInt, JsFalse]
class AutoSlideMethod(Str, Enum): # type: ignore
null = "null"
MouseWheel = Union[JsNull, float]
class Transition(Str, Enum): # type: ignore
none = "none"
fade = "fade"
slide = "slide"
convex = "convex"
concave = "concave"
zoom = "zoom"
class TransitionSpeed(Str, Enum): # type: ignore
default = "default"
fast = "fast"
slow = "slow"
BackgroundTransition = Transition
class Display(Str, Enum): # type: ignore
block = "block"
class RevealTheme(str, Enum):
black = "black"
white = "white"
league = "league"
beige = "beige"
sky = "sky"
night = "night"
serif = "serif"
simple = "simple"
soralized = "solarized"
blood = "blood"
moon = "moon"
class RevealJS(Converter):
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
margin: float = 0.04
min_scale: float = 0.2
max_scale: float = 2.0
# Configuration options from RevealJS
controls: JsBool = JsBool.false
controls_tutorial: JsBool = JsBool.true
controls_layout: ControlsLayout = ControlsLayout.bottom_right
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
progress: JsBool = JsBool.false
slide_number: SlideNumber = SlideNumber.false
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
hash_one_based_index: JsBool = JsBool.false
hash: JsBool = JsBool.false
respond_to_hash_changes: JsBool = JsBool.false
history: JsBool = JsBool.false
keyboard: JsBool = JsBool.true
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
disable_layout: JsBool = JsBool.false
overview: JsBool = JsBool.true
center: JsBool = JsBool.true
touch: JsBool = JsBool.true
loop: JsBool = JsBool.false
rtl: JsBool = JsBool.false
navigation_mode: NavigationMode = NavigationMode.default
shuffle: JsBool = JsBool.false
fragments: JsBool = JsBool.true
fragment_in_url: JsBool = JsBool.true
embedded: JsBool = JsBool.false
help: JsBool = JsBool.true
pause: JsBool = JsBool.true
show_notes: JsBool = JsBool.false
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
preload_iframes: PreloadIframes = PreloadIframes.null
auto_animate: JsBool = JsBool.true
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
auto_animate_duration: float = 1.0
auto_animate_unmatched: JsBool = JsBool.true
auto_animate_styles: List[str] = [
"opacity",
"color",
"background-color",
"padding",
"font-size",
"line-height",
"letter-spacing",
"border-width",
"border-color",
"border-radius",
"outline",
"outline-offset",
]
auto_slide: AutoSlide = 0
auto_slide_stoppable: JsBool = JsBool.true
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
default_timing: Union[JsNull, int] = JsNull.null
mouse_wheel: JsBool = JsBool.false
preview_links: JsBool = JsBool.false
post_message: JsBool = JsBool.true
post_message_events: JsBool = JsBool.false
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_transition: BackgroundTransition = BackgroundTransition.none
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
pdf_separate_fragments: JsBool = JsBool.true
pdf_page_height_offset: int = -1
view_distance: int = 3
mobile_view_distance: int = 2
display: Display = Display.block
hide_inactive_cursor: JsBool = JsBool.true
hide_cursor_time: int = 5000
# Add. options
background_color: str = "black" # TODO: use pydantic.color.Color
reveal_version: str = "4.4.0"
reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides"
class Config:
use_enum_values = True
extra = "forbid"
def get_sections_iter(self) -> Generator[str, None, None]:
"""Generates a sequence of sections, one per slide, that will be included into the html template."""
for presentation_config in self.presentation_configs:
for slide_config in presentation_config.slides:
file = presentation_config.files[slide_config.start_animation]
file = os.path.join(self.assets_dir, os.path.basename(file))
# TODO: document this
# Videos are muted because, otherwise, the first slide never plays correctly.
# This is due to a restriction in playing audio without the user doing anything.
# Later, this might be useful to only mute the first video, or to make it optional.
if slide_config.is_loop():
yield f'<section data-background-video="{file}" data-background-video-muted data-background-video-loop></section>'
else:
yield f'<section data-background-video="{file}" data-background-video-muted></section>'
def load_template(self) -> str:
"""Returns the RevealJS HTML template as a string."""
if isinstance(self.template, str):
with open(self.template, "r") as f:
return f.read()
return pkg_resources.resource_string(
__name__, "data/revealjs_template.html"
).decode()
def open(self, file: str) -> bool:
return webbrowser.open(file)
def convert_to(self, dest: str) -> None:
"""Converts this configuration into a RevealJS HTML presentation, saved to DEST."""
dirname = os.path.dirname(dest)
basename, ext = os.path.splitext(os.path.basename(dest))
self.assets_dir = self.assets_dir.format(
dirname=dirname, basename=basename, ext=ext
)
full_assets_dir = os.path.join(dirname, self.assets_dir)
os.makedirs(full_assets_dir, exist_ok=True)
for presentation_config in self.presentation_configs:
presentation_config.concat_animations().move_to(full_assets_dir)
with open(dest, "w") as f:
sections = "".join(self.get_sections_iter())
revealjs_template = self.load_template()
content = revealjs_template.format(sections=sections, **self.dict())
f.write(content)
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wraps a function to add a `--show-config` option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
converter = Converter.from_string(to)(presentation_configs=[])
for key, value in converter.dict().items():
click.echo(f"{key}: {repr(value)}")
ctx.exit()
return click.option(
"--show-config",
is_flag=True,
help="Show supported options for given format and exit.",
default=None,
expose_value=False,
show_envvar=True,
callback=callback,
)(function)
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wraps a function to add a `--show-template` option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
template = ctx.params.get("template", None)
converter = Converter.from_string(to)(
presentation_configs=[], template=template
)
click.echo(converter.load_template())
ctx.exit()
return click.option(
"--show-template",
is_flag=True,
help="Show the template (currently) used for a given conversion format and exit.",
default=None,
expose_value=False,
show_envvar=True,
callback=callback,
)(function)
@click.command()
@click.argument("scenes", nargs=-1)
@folder_path_option
@click.argument("dest")
@click.option(
"--to",
type=click.Choice(["html"], case_sensitive=False),
default="html",
show_default=True,
help="Set the conversion format to use.",
)
@click.option(
"--open",
"open_result",
is_flag=True,
help="Open the newly created file using the approriate application.",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
@click.option(
"-c",
"--config",
"config_options",
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. E.g., pass `-cslide_number=true` to display slide numbers.",
)
@click.option(
"--use-template",
"template",
metavar="FILE",
type=click.Path(exists=True, dir_okay=False),
help="Use the template given by FILE instead of default one. To echo the default template, use `--show-template`.",
)
@show_template_option
@show_config_options
@verbosity_option
def convert(
scenes: List[str],
folder: str,
dest: str,
to: str,
open_result: bool,
force: bool,
config_options: Dict[str, str],
template: Optional[str],
) -> None:
"""
Convert SCENE(s) into a given format and writes the result in DEST.
"""
presentation_configs = get_scenes_presentation_config(scenes, folder)
try:
converter = Converter.from_string(to)(
presentation_configs=presentation_configs,
template=template,
**config_options,
)
converter.convert_to(dest)
if open_result:
converter.open(dest)
except ValidationError as e:
errors = e.errors()
msg = [
f"{len(errors)} error(s) occured with configuration options for '{to}', see below."
]
for error in errors:
option = error["loc"][0]
_msg = error["msg"]
msg.append(f"Option '{option}': {_msg}")
raise click.UsageError("\n".join(msg))

View File

@ -0,0 +1,291 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{title}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{reveal_version}/reveal.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{reveal_version}/theme/{reveal_theme}.min.css">
<!-- Theme used for syntax highlighting of code -->
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
<!-- <link rel="stylesheet" href="index.css"> -->
</head>
<body>
<div class="reveal">
<div class="slides">
{sections}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{reveal_version}/reveal.min.js"></script>
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({{
// The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different
// resolutions. Can be specified using percentage units.
width: {width},
height: {height},
// Factor of the display size that should remain empty around
// the content
margin: {margin},
// Bounds for smallest/largest possible scale to apply to content
minScale: {min_scale},
maxScale: {max_scale},
// Display presentation control arrows
controls: {controls},
// Help the user learn the controls by providing hints, for example by
// bouncing the down arrow when they first encounter a vertical slide
controlsTutorial: {controls_tutorial},
// Determines where controls appear, "edges" or "bottom-right"
controlsLayout: {controls_layout},
// Visibility rule for backwards navigation arrows; "faded", "hidden"
// or "visible"
controlsBackArrows: {controls_back_arrows},
// Display a presentation progress bar
progress: {progress},
// Display the page number of the current slide
// - true: Show slide number
// - false: Hide slide number
//
// Can optionally be set as a string that specifies the number formatting:
// - "h.v": Horizontal . vertical slide number (default)
// - "h/v": Horizontal / vertical slide number
// - "c": Flattened slide number
// - "c/t": Flattened slide number / total slides
//
// Alternatively, you can provide a function that returns the slide
// number for the current slide. The function should take in a slide
// object and return an array with one string [slideNumber] or
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
slideNumber: {slide_number},
// Can be used to limit the contexts in which the slide number appears
// - "all": Always show the slide number
// - "print": Only when printing to PDF
// - "speaker": Only in the speaker view
showSlideNumber: {show_slide_number},
// Use 1 based indexing for # links to match slide number (default is zero
// based)
hashOneBasedIndex: {hash_one_based_index},
// Add the current slide number to the URL hash so that reloading the
// page/copying the URL will return you to the same slide
hash: {hash},
// Flags if we should monitor the hash and change slides accordingly
respondToHashChanges: {respond_to_hash_changes},
// Push each slide change to the browser history. Implies `hash: true`
history: {history},
// Enable keyboard shortcuts for navigation
keyboard: {keyboard},
// Optional function that blocks keyboard events when retuning false
//
// If you set this to 'focused', we will only capture keyboard events
// for embedded decks when they are in focus
keyboardCondition: {keyboard_condition},
// Disables the default reveal.js slide layout (scaling and centering)
// so that you can use custom CSS layout
disableLayout: {disable_layout},
// Enable the slide overview mode
overview: {overview},
// Vertical centering of slides
center: {center},
// Enables touch navigation on devices with touch input
touch: {touch},
// Loop the presentation
loop: {loop},
// Change the presentation direction to be RTL
rtl: {rtl},
// Changes the behavior of our navigation directions.
//
// "default"
// Left/right arrow keys step between horizontal slides, up/down
// arrow keys step between vertical slides. Space key steps through
// all slides (both horizontal and vertical).
//
// "linear"
// Removes the up/down arrows. Left/right arrows step through all
// slides (both horizontal and vertical).
//
// "grid"
// When this is enabled, stepping left/right from a vertical stack
// to an adjacent vertical stack will land you at the same vertical
// index.
//
// Consider a deck with six slides ordered in two vertical stacks:
// 1.1 2.1
// 1.2 2.2
// 1.3 2.3
//
// If you're on slide 1.3 and navigate right, you will normally move
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
// from 1.3 -> 2.3.
navigationMode: {navigation_mode},
// Randomizes the order of slides each time the presentation loads
shuffle: {shuffle},
// Turns fragments on and off globally
fragments: {fragments},
// Flags whether to include the current fragment in the URL,
// so that reloading brings you to the same fragment position
fragmentInURL: {fragment_in_url},
// Flags if the presentation is running in an embedded mode,
// i.e. contained within a limited portion of the screen
embedded: {embedded},
// Flags if we should show a help overlay when the question-mark
// key is pressed
help: {help},
// Flags if it should be possible to pause the presentation (blackout)
pause: {pause},
// Flags if speaker notes should be visible to all viewers
showNotes: {show_notes},
// Global override for autolaying embedded media (video/audio/iframe)
// - null: Media will only autoplay if data-autoplay is present
// - true: All media will autoplay, regardless of individual setting
// - false: No media will autoplay, regardless of individual setting
autoPlayMedia: {auto_play_media},
// Global override for preloading lazy-loaded iframes
// - null: Iframes with data-src AND data-preload will be loaded when within
// the viewDistance, iframes with only data-src will be loaded when visible
// - true: All iframes with data-src will be loaded when within the viewDistance
// - false: All iframes with data-src will be loaded only when visible
preloadIframes: {preload_iframes},
// Can be used to globally disable auto-animation
autoAnimate: {auto_animate},
// Optionally provide a custom element matcher that will be
// used to dictate which elements we can animate between.
autoAnimateMatcher: {auto_animate_matcher},
// Default settings for our auto-animate transitions, can be
// overridden per-slide or per-element via data arguments
autoAnimateEasing: {auto_animate_easing},
autoAnimateDuration: {auto_animate_duration},
autoAnimateUnmatched: {auto_animate_unmatched},
// CSS properties that can be auto-animated. Position & scale
// is matched separately so there's no need to include styles
// like top/right/bottom/left, width/height or margin.
autoAnimateStyles: {auto_animate_styles},
// Controls automatic progression to the next slide
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
// is present on the current slide or fragment
// - 1+: All slides will progress automatically at the given interval
// - false: No auto-sliding, even if data-autoslide is present
autoSlide: {auto_slide},
// Stop auto-sliding after user input
autoSlideStoppable: {auto_slide_stoppable},
// Use this method for navigation when auto-sliding (defaults to navigateNext)
autoSlideMethod: {auto_slide_method},
// Specify the average time in seconds that you think you will spend
// presenting each slide. This is used to show a pacing timer in the
// speaker view
defaultTiming: {default_timing},
// Enable slide navigation via mouse wheel
mouseWheel: {mouse_wheel},
// Opens links in an iframe preview overlay
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
// individually
previewLinks: {preview_links},
// Exposes the reveal.js API through window.postMessage
postMessage: {post_message},
// Dispatches all reveal.js events to the parent window through postMessage
postMessageEvents: {post_message_events},
// Focuses body when page changes visibility to ensure keyboard shortcuts work
focusBodyOnPageVisibilityChange: {focus_body_on_page_visibility_change},
// Transition style
transition: {transition}, // none/fade/slide/convex/concave/zoom
// Transition speed
transitionSpeed: {transition_speed}, // default/fast/slow
// Transition style for full page slide backgrounds
backgroundTransition: {background_transition}, // none/fade/slide/convex/concave/zoom
// The maximum number of pages a single slide can expand onto when printing
// to PDF, unlimited by default
pdfMaxPagesPerSlide: {pdf_max_pages_per_slide},
// Prints each fragment on a separate slide
pdfSeparateFragments: {pdf_separate_fragments},
// Offset used to reduce the height of content within exported PDF pages.
// This exists to account for environment differences based on how you
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
// on precisely the total height of the document whereas in-browser
// printing has to end one pixel before.
pdfPageHeightOffset: {pdf_page_height_offset},
// Number of slides away from the current that are visible
viewDistance: {view_distance},
// Number of slides away from the current that are visible on mobile
// devices. It is advisable to set this to a lower number than
// viewDistance in order to save resources.
mobileViewDistance: {mobile_view_distance},
// The display mode that will be used to show slides
display: {display},
// Hide cursor if inactive
hideInactiveCursor: {hide_inactive_cursor},
// Time before the cursor is hidden (in ms)
hideCursorTime: {hide_cursor_time}
}});
</script>
</body>
</html>

2
manim_slides/defaults.py Normal file
View File

@ -0,0 +1,2 @@
FOLDER_PATH: str = "./slides"
CONFIG_PATH: str = ".manim-slides.json"

81
manim_slides/manim.py Normal file
View File

@ -0,0 +1,81 @@
import os
import sys
from contextlib import contextmanager
from importlib.util import find_spec
from typing import Iterator
__all__ = [
"MANIM",
"MANIM_PACKAGE_NAME",
"MANIM_AVAILABLE",
"MANIM_IMPORTED",
"MANIMGL",
"MANIMGL_PACKAGE_NAME",
"MANIMGL_AVAILABLE",
"MANIMGL_IMPORTED",
"logger",
"Scene",
"ThreeDScene",
"config",
"FFMPEG_BIN",
]
@contextmanager
def suppress_stdout() -> Iterator[None]:
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"
)
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
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable

862
manim_slides/present.py Normal file
View File

@ -0,0 +1,862 @@
import os
import platform
import sys
import time
from enum import Enum, IntEnum, auto, unique
from typing import Any, Dict, List, Optional, Tuple, Union
import click
import cv2
import numpy as np
from pydantic import ValidationError
from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
from tqdm import tqdm
from .commons import config_path_option, verbosity_option
from .config import DEFAULT_CONFIG, Config, PresentationConfig, SlideConfig
from .defaults import FOLDER_PATH
from .manim import logger
from .resources import * # noqa: F401, F403
os.environ.pop(
"QT_QPA_PLATFORM_PLUGIN_PATH", None
) # See why here: https://stackoverflow.com/a/67863156
WINDOW_NAME = "Manim Slides"
WINDOW_INFO_NAME = f"{WINDOW_NAME}: Info"
WINDOWS = platform.system() == "Windows"
class AspectRatio(Enum):
ignore = Qt.IgnoreAspectRatio
keep = Qt.KeepAspectRatio
auto = "auto"
ASPECT_RATIO_MODES = {
"ignore": AspectRatio.ignore,
"keep": AspectRatio.keep,
"auto": AspectRatio.auto,
}
RESIZE_MODES = {
"fast": Qt.FastTransformation,
"smooth": Qt.SmoothTransformation,
}
@unique
class State(IntEnum):
"""Represents all possible states of a slide presentation."""
PLAYING = auto()
PAUSED = auto()
WAIT = auto()
END = auto()
def __str__(self) -> str:
return self.name.capitalize()
def now() -> float:
"""Returns time.time() in seconds."""
return time.time()
class Presentation:
"""Creates presentation from a configuration object."""
def __init__(self, config: PresentationConfig) -> None:
self.slides: List[SlideConfig] = config.slides
self.files: List[str] = config.files
self.current_slide_index: int = 0
self.current_animation: int = self.current_slide.start_animation
self.current_file: str = ""
self.loaded_animation_cap: int = -1
self.cap = None # cap = cv2.VideoCapture
self.reverse: bool = False
self.reversed_animation: int = -1
self.lastframe: Optional[np.ndarray] = None
self.reset()
@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) -> None:
"""Releases current Video Capture, if existing."""
if self.cap is not None:
self.cap.release()
self.loaded_animation_cap = -1
def load_animation_cap(self, animation: int) -> None:
"""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
logger.debug(f"Loading new cap for animation #{animation}")
self.release_cap()
file: str = 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) -> None:
"""Rewinds current slide to first frame."""
logger.debug("Rewinding curring slide")
if self.reverse:
self.current_animation = self.current_slide.end_animation - 1
else:
self.current_animation = self.current_slide.start_animation
cap = self.current_cap
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def cancel_reverse(self) -> None:
"""Cancels any effet produced by a reversed slide."""
if self.reverse:
logger.debug("Cancelling effects from previous 'reverse' action'")
self.reverse = False
self.reversed_animation = -1
self.release_cap()
def reverse_current_slide(self) -> None:
"""Reverses current slide."""
self.reverse = True
self.rewind_current_slide()
def load_next_slide(self) -> None:
"""Loads next slide."""
logger.debug("Loading 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) -> None:
"""Loads previous slide."""
logger.debug("Loading 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."""
fps = self.current_cap.get(cv2.CAP_PROP_FPS)
if fps == 0:
logger.warn(
f"Something is wrong with video file {self.current_file}, as the fps returned by frame {self.current_frame_number} is 0"
)
return max(fps, 1) # TODO: understand why we sometimes get 0 fps
def reset(self) -> None:
"""Rests current presentation."""
self.current_animation = 0
self.load_animation_cap(0)
self.current_slide_index = 0
self.slides[-1].terminated = False
def load_last_slide(self) -> None:
"""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:
return self.current_animation + 1
@property
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_frame_number(self) -> int:
"""Returns current frame number."""
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
def update_state(self, state: State) -> Tuple[np.ndarray, State]:
"""
Updates the current state given the previous one.
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 self.lastframe is None:
_, self.lastframe = self.current_cap.read()
return self.lastframe, state
still_playing, frame = self.current_cap.read()
if still_playing:
self.lastframe = frame
elif state == state.WAIT or state == state.PAUSED: # type: ignore
return self.lastframe, state
elif self.current_slide.is_last() and self.current_slide.terminated:
return self.lastframe, State.END
else: # not still playing
if self.is_last_animation:
if self.current_slide.is_slide():
state = State.WAIT
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.is_last()
and self.current_slide.end_animation == self.current_animation
):
state = State.WAIT
else:
# Play next video!
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)
return self.lastframe, state
class Display(QThread): # type: ignore
"""Displays one or more presentations one after each other."""
change_video_signal = Signal(np.ndarray)
change_info_signal = Signal(dict)
finished = Signal()
def __init__(
self,
presentations: List[PresentationConfig],
config: Config = DEFAULT_CONFIG,
start_paused: bool = False,
skip_all: bool = False,
record_to: Optional[str] = None,
exit_after_last_slide: bool = False,
) -> None:
super().__init__()
self.presentations = presentations
self.start_paused = start_paused
self.config = config
self.skip_all = skip_all
self.record_to = record_to
self.recordings: List[Tuple[str, int, int]] = []
self.state = State.PLAYING
self.lastframe: Optional[np.ndarray] = None
self.current_presentation_index = 0
self.run_flag = True
self.key = -1
self.exit_after_last_slide = exit_after_last_slide
@property
def current_presentation(self) -> Presentation:
"""Returns the current presentation."""
return self.presentations[self.current_presentation_index]
def run(self) -> None:
"""Runs a series of presentations until end or exit."""
while self.run_flag:
last_time = now()
self.lastframe, self.state = self.current_presentation.update_state(
self.state
)
if self.state == State.PLAYING or self.state == State.PAUSED:
if self.start_paused:
self.state = State.PAUSED
self.start_paused = False
if self.state == State.END:
if self.current_presentation_index == len(self.presentations) - 1:
if self.exit_after_last_slide:
self.run_flag = False
continue
else:
self.current_presentation_index += 1
self.state = State.PLAYING
self.handle_key()
self.show_video()
self.show_info()
lag = now() - last_time
sleep_time = 1 / self.current_presentation.fps
sleep_time = max(sleep_time - lag, 0)
time.sleep(sleep_time)
last_time = now()
self.current_presentation.release_cap()
if self.record_to is not None:
self.record_movie()
logger.debug("Closing video thread gracully and exiting")
self.finished.emit()
def record_movie(self) -> None:
logger.debug(
f"A total of {len(self.recordings)} frames will be saved to {self.record_to}"
)
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()
def show_video(self) -> None:
"""Shows updated video."""
if self.record_to is not None:
pres = self.current_presentation
self.recordings.append(
(pres.current_file, pres.current_frame_number, pres.fps)
)
frame: np.ndarray = self.lastframe
self.change_video_signal.emit(frame)
def show_info(self) -> None:
"""Shows updated information about presentations."""
self.change_info_signal.emit(
{
"animation": self.current_presentation.current_animation,
"state": self.state,
"slide_index": self.current_presentation.current_slide.number,
"n_slides": len(self.current_presentation.slides),
"type": self.current_presentation.current_slide.type,
"scene_index": self.current_presentation_index + 1,
"n_scenes": len(self.presentations),
}
)
@Slot(int)
def set_key(self, key: int) -> None:
"""Sets the next key to be handled."""
self.key = key
def handle_key(self) -> None:
"""Handles key strokes."""
key = self.key
if self.config.QUIT.match(key):
self.run_flag = False
elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key):
self.state = State.PAUSED
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
self.state = State.PLAYING
elif self.state == State.WAIT and (
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
):
self.current_presentation.load_next_slide()
self.state = State.PLAYING
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_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.load_previous_slide()
self.state = State.PLAYING
elif self.config.REVERSE.match(key):
self.current_presentation.reverse_current_slide()
self.state = State.PLAYING
elif self.config.REWIND.match(key):
self.current_presentation.cancel_reverse()
self.current_presentation.rewind_current_slide()
self.state = State.PLAYING
self.key = -1 # No more key to be handled
def stop(self) -> None:
"""Stops current thread, without doing anything after."""
self.run_flag = False
self.wait()
class Info(QWidget): # type: ignore
def __init__(self) -> None:
super().__init__()
self.setWindowTitle(WINDOW_INFO_NAME)
self.layout = QGridLayout()
self.setLayout(self.layout)
self.animationLabel = QLabel()
self.stateLabel = QLabel()
self.slideLabel = QLabel()
self.typeLabel = QLabel()
self.sceneLabel = QLabel()
self.layout.addWidget(self.animationLabel, 0, 0, 1, 2)
self.layout.addWidget(self.stateLabel, 1, 0)
self.layout.addWidget(self.slideLabel, 1, 1)
self.layout.addWidget(self.typeLabel, 2, 0)
self.layout.addWidget(self.sceneLabel, 2, 1)
self.update_info({})
@Slot(dict)
def update_info(self, info: Dict[str, Union[str, int]]) -> None:
self.animationLabel.setText("Animation: {}".format(info.get("animation", "na")))
self.stateLabel.setText("State: {}".format(info.get("state", "unknown")))
self.slideLabel.setText(
"Slide: {}/{}".format(
info.get("slide_index", "na"), info.get("n_slides", "na")
)
)
self.typeLabel.setText("Slide Type: {}".format(info.get("type", "unknown")))
self.sceneLabel.setText(
"Scene: {}/{}".format(
info.get("scene_index", "na"), info.get("n_scenes", "na")
)
)
class InfoThread(QThread): # type: ignore
def __init__(self) -> None:
super().__init__()
self.dialog = Info()
self.run_flag = True
def start(self) -> None:
super().start()
self.dialog.show()
def stop(self) -> None:
self.dialog.deleteLater()
class App(QWidget): # type: ignore
send_key_signal = Signal(int)
def __init__(
self,
*args: Any,
config: Config = DEFAULT_CONFIG,
fullscreen: bool = False,
resolution: Tuple[int, int] = (1980, 1080),
hide_mouse: bool = False,
aspect_ratio: AspectRatio = AspectRatio.auto,
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
background_color: str = "black",
**kwargs: Any,
):
super().__init__()
self.setWindowTitle(WINDOW_NAME)
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.display_width, self.display_height = resolution
self.aspect_ratio = aspect_ratio
self.resize_mode = resize_mode
self.hide_mouse = hide_mouse
self.config = config
if self.hide_mouse:
self.setCursor(Qt.BlankCursor)
self.label = QLabel(self)
if self.aspect_ratio == AspectRatio.auto:
self.label.setScaledContents(True)
self.label.setAlignment(Qt.AlignCenter)
self.label.resize(self.display_width, self.display_height)
self.label.setStyleSheet(f"background-color: {background_color}")
self.pixmap = QPixmap(self.width(), self.height())
self.label.setPixmap(self.pixmap)
self.label.setMinimumSize(1, 1)
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
# create the info dialog
self.info = Info()
self.info.show()
# info widget will also listen to key presses
self.info.keyPressEvent = self.keyPressEvent
if fullscreen:
self.showFullScreen()
# connect signals
self.thread.change_video_signal.connect(self.update_image)
self.thread.change_info_signal.connect(self.info.update_info)
self.thread.finished.connect(self.closeAll)
self.send_key_signal.connect(self.thread.set_key)
# start the thread
self.thread.start()
def keyPressEvent(self, event: QKeyEvent) -> None:
key = event.key()
if self.config.HIDE_MOUSE.match(key):
if self.hide_mouse:
self.setCursor(Qt.ArrowCursor)
self.hide_mouse = False
else:
self.setCursor(Qt.BlankCursor)
self.hide_mouse = True
# We send key to be handled by video display
self.send_key_signal.emit(key)
event.accept()
def closeAll(self) -> None:
logger.debug("Closing all QT windows")
self.thread.stop()
self.info.deleteLater()
self.deleteLater()
def resizeEvent(self, event: QResizeEvent) -> None:
if not self.label.hasScaledContents():
self.pixmap = self.pixmap.scaled(
self.width(), self.height(), self.aspect_ratio.value, self.resize_mode
)
self.label.setPixmap(self.pixmap)
self.label.resize(self.width(), self.height())
def closeEvent(self, event: QCloseEvent) -> None:
self.closeAll()
event.accept()
@Slot(np.ndarray)
def update_image(self, cv_img: np.ndarray) -> None:
"""Updates the (image) label with a new opencv image"""
h, w, ch = cv_img.shape
bytes_per_line = ch * w
qt_img = QImage(cv_img.data, w, h, bytes_per_line, QImage.Format_BGR888)
if not self.label.hasScaledContents() and (
w != self.width() or h != self.height()
):
qt_img = qt_img.scaled(
self.width(), self.height(), self.aspect_ratio.value, self.resize_mode
)
self.label.setPixmap(QPixmap.fromImage(qt_img))
@click.command()
@click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
show_default=True,
)
@click.help_option("-h", "--help")
@verbosity_option
def list_scenes(folder: str) -> None:
"""List available scenes."""
for i, scene in enumerate(_list_scenes(folder), start=1):
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder: str) -> List[str]:
"""Lists available scenes in given directory."""
scenes = []
for file in os.listdir(folder):
if file.endswith(".json"):
filepath = os.path.join(folder, file)
try:
_ = PresentationConfig.parse_file(filepath)
scenes.append(os.path.basename(file)[:-5])
except Exception as e: # Could not parse this file as a proper presentation config
logger.warn(
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
)
pass
logger.debug(f"Found {len(scenes)} valid scene configuration files in `{folder}`.")
return scenes
def prompt_for_scenes(folder: str) -> List[str]:
"""Prompts the user to select scenes within a given folder."""
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
for i, scene in scene_choices.items():
click.secho(f"{i}: {scene}", fg="green")
click.echo()
click.echo("Choose number corresponding to desired scene/arguments.")
click.echo("(Use comma separated list for multiple entries)")
def value_proc(value: Optional[str]) -> List[str]:
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
if not all(0 < i <= len(scene_choices) for i in indices):
raise click.UsageError("Please only enter numbers displayed on the screen.")
return [scene_choices[i] for i in indices]
if len(scene_choices) == 0:
raise click.UsageError(
"No scenes were found, are you in the correct directory?"
)
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
return scenes
except ValueError as e:
raise click.UsageError(str(e))
def get_scenes_presentation_config(
scenes: List[str], folder: str
) -> List[PresentationConfig]:
"""Returns a list of presentation configurations based on the user input."""
if len(scenes) == 0:
scenes = prompt_for_scenes(folder)
presentation_configs = []
for scene in scenes:
config_file = os.path.join(folder, f"{scene}.json")
if not os.path.exists(config_file):
raise click.UsageError(
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
)
try:
presentation_configs.append(PresentationConfig.parse_file(config_file))
except ValidationError as e:
raise click.UsageError(str(e))
return presentation_configs
@click.command()
@click.argument("scenes", nargs=-1)
@config_path_option
@click.option(
"--folder",
metavar="DIRECTORY",
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(
"-s",
"--skip-all",
is_flag=True,
help="Skip all slides, useful the test if slides are working. Automatically sets `--skip-after-last-slide` to True.",
)
@click.option(
"-r",
"--resolution",
metavar="<WIDTH HEIGHT>",
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(
"--to",
"--record-to",
"record_to",
metavar="FILE",
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.option(
"--exit-after-last-slide",
is_flag=True,
help="At the end of last slide, the application will be exited.",
)
@click.option(
"--hide-mouse",
is_flag=True,
help="Hide mouse cursor.",
)
@click.option(
"--aspect-ratio",
type=click.Choice(ASPECT_RATIO_MODES.keys(), case_sensitive=False),
default="auto",
help="Set the aspect ratio mode to be used when rescaling video. `'auto'` option is equivalent to `'ignore'`, but can be much faster due to not calling `scaled()` method on every frame.",
show_default=True,
)
@click.option(
"--resize-mode",
type=click.Choice(RESIZE_MODES.keys(), case_sensitive=False),
default="smooth",
help="Set the resize (i.e., transformation) mode to be used when rescaling video.",
show_default=True,
)
@click.option(
"--background-color",
"--bgcolor",
"background_color",
metavar="COLOR",
type=str,
default="black",
help='Set the background color for borders when using "keep" resize mode. Can be any valid CSS color, e.g., "green", "#FF6500" or "rgba(255, 255, 0, .5)".',
show_default=True,
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
scenes: List[str],
config_path: str,
folder: str,
start_paused: bool,
fullscreen: bool,
skip_all: bool,
resolution: Tuple[int, int],
record_to: Optional[str],
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
resize_mode: str,
background_color: str,
) -> None:
"""
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 skip_all:
exit_after_last_slide = True
presentations = [
Presentation(presentation_config)
for presentation_config in get_scenes_presentation_config(scenes, folder)
]
if os.path.exists(config_path):
try:
config = Config.parse_file(config_path)
except ValidationError as e:
raise click.UsageError(str(e))
else:
logger.debug("No configuration file found, using default configuration.")
config = Config()
if record_to is not None:
_, ext = os.path.splitext(record_to)
if ext.lower() != ".avi":
raise click.UsageError(
"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
)
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides")
a = App(
presentations,
config=config,
start_paused=start_paused,
fullscreen=fullscreen,
skip_all=skip_all,
resolution=resolution,
record_to=record_to,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio=ASPECT_RATIO_MODES[aspect_ratio],
resize_mode=RESIZE_MODES[resize_mode],
background_color=background_color,
)
a.show()
sys.exit(app.exec_())
logger.debug("After")

171
manim_slides/resources.py Normal file
View File

@ -0,0 +1,171 @@
# type: ignore
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.4.0
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
qt_resource_data = b"\
\x00\x00\x08\x1c\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x01\x00\x00\x00\x01\x00\x08\x06\x00\x00\x00\x5cr\xa8f\
\x00\x00\x01\x84iCCPICC prof\
ile\x00\x00(\x91}\x91=H\xc3@\x1c\xc5_\
S\xa5R+\x0e\xed \xe2\x90\xa1:Y\x10-\xe2\xa8\
U(B\x85P+\xb4\xea`r\xfd\x84&\x0dI\x8a\
\x8b\xa3\xe0Zp\xf0c\xb1\xea\xe0\xe2\xac\xab\x83\xab \
\x08~\x808:9)\xbaH\x89\xffK\x0a-b<\
8\xee\xc7\xbb{\x8f\xbbw\x80\xd0\xac2\xd5\xec\x99\x00\
T\xcd2\xd2\xc9\x84\x98\xcd\xad\x8a\x81W\x04\x11F?\
\xe2\x88\xcb\xcc\xd4\xe7$)\x05\xcf\xf1u\x0f\x1f_\xef\
b<\xcb\xfb\xdc\x9fc _0\x19\xe0\x13\x89g\x99\
nX\xc4\x1b\xc4\xd3\x9b\x96\xcey\x9f8\xc2\xcar\x9e\
\xf8\x9cx\xdc\xa0\x0b\x12?r]q\xf9\x8ds\xc9a\
\x81gF\x8cLz\x9e8B,\x96\xbaX\xe9bV\
6T\xe28q4\xafj\x94/d]\xces\xde\xe2\
\xacV\xeb\xac}O\xfe\xc2PA[Y\xe6:\xcd\x11\
$\xb1\x88%H\x10\xa1\xa0\x8e\x0a\xaa\xb0\x10\xa3U#\
\xc5D\x9a\xf6\x13\x1e\xfea\xc7/\x91K!W\x05\x8c\
\x1c\x0b\xa8A\x85\xec\xf8\xc1\xff\xe0w\xb7fqj\xd2\
M\x0a%\x80\xde\x17\xdb\xfe\x18\x05\x02\xbb@\xaba\xdb\
\xdf\xc7\xb6\xdd:\x01\xfc\xcf\xc0\x95\xd6\xf1\xd7\x9a\xc0\xcc\
'\xe9\x8d\x8e\x16=\x02\x06\xb7\x81\x8b\xeb\x8e\xa6\xec\x01\
\x97;\xc0\xd0\x93.\x1b\xb2#\xf9i\x0a\xc5\x22\xf0~\
F\xdf\x94\x03\xc2\xb7@p\xcd\xed\xad\xbd\x8f\xd3\x07 \
C]\xa5n\x80\x83C`\xacD\xd9\xeb\x1e\xef\xee\xeb\
\xee\xed\xdf3\xed\xfe~\x00\xd6\xd3r\xcf+\xa2\xc1_\
\x00\x00\x00\x06bKGD\x004\x004\x004\xaf4\
\x1c\xc0\x00\x00\x00\x09pHYs\x00\x00.#\x00\x00\
.#\x01x\xa5?v\x00\x00\x00\x07tIME\x07\
\xe6\x0a\x13\x0c\x0f\x03\x13^\x06\xfe\x00\x00\x00\x19tE\
XtComment\x00Create\
d with GIMPW\x81\x0e\x17\x00\
\x00\x05\xf4IDATx\xda\xed\xddA\x92\x9b:\x18\
\x85QK\xe5%\xc1\xfe\x17\x00{rF\x19\xa4\xcb\xee\
\x801X\xd2=g\x98z\x956 }\xfe\xd5\xddy\
\xbe\xdd\x00\x00\x00\x00\x00\x801\x95\xab\xbe\xd04M\x0f\
\xb7\x1b\xb6[\xd7\xb5t\x1d\x00\x9b\x1e\xda\x8eA\xb1\xf1\
!7\x04\xc5\xc6\x87\xdc\x10\x14\x1b\x1fr#Pl~\
\xc8\x0dA\xb1\xf9!7\x02\xc5\xe6\x87\xdc\x08\x14\x9b\x1f\
r#Pl~\xc8\x8d@\xb1\xf9!7\x02\xc5\xe6\x87\
\xdc\x08T\x9b\x1frU\xb7\x00\xc6\xb4\xe5\x8d\xbb|\xe2\
/\x01\xfa<\x0aT\x9b\x1f\x1c\x01\x80\xc0\xa3@\xf5\xee\
\x0f&\x00 p\x0a\xa8\xde\xfd\xc1\x04\x00\x04N\x01\x02\
\x00&\x00@\x00\x9c\xff!\xca\xbd\x85\x17\xb1,\x8b'\
A\x9cy\x9e\xb3\x03`\xe3\x93\xec\xef\xfa\xfff\x08\xaa\
\xcd\x0f\xb9\x13p\xb5\xf9!7\x02~\x0a\x00\xc1.\x0f\
\x80w\x7fhgo\x98\x00\xc0\x04\x00\x08\x00 \x00\x80\
\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\x00 \
\x00\x80\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\
\x00 \x00\x80\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\
\x00|\xd2\xbd\xb7\x17\xfc\xdbG)\x8f\xfc\xb1c-^\
\xb7g\xd1\xffu\x97\x9f\x7f0M\xd3\xe3\xcc/x\xe4\
\x06m\xfd\x1c\xf5\x91\x16_\x8b\xd7\xbc\xe7\xf3\xec\x13\x9f\
\xc5\x91\xeb\xde\xf35\xde\xb1\xaek\xe9\xf2\x08\xb0\xe7\xc6\
\x9c}\x13\x93\xafy\xef\xd7I|\x16=]w\x1d\xf1\
\xe6\x8f\xb0\xf0Z\xbc\xe6w\xff\xfe\xc4g\xd1\x8b\xea\xe6\
\xb7w\xbdG\xae\xb9\xd5\xfb\x95\xf6\x1c{\xb9\xe6\xea\x01\
x\xbd\xae/\xf7\x99\xf81\xa0\x85v\xe9\xeb;:\xe1\
\x006\xbfkF\x00l\x04\xd7\x8e\x00t\xbc\xf8m\
\x00\x11\x10\x00\x8b\xde\xfd\x10C\x01\xb0\xf9qo\x04\xc0\
\x02w\x8f\xdc\x04\x01\xb0\xb0\xdd+\x04\xc0\xf9V\x04\x10\
\x00\x8bX<\x11\x00\x9b\xdf\xbdD\x00,X\xf7\x14\x01\
\xb0P\xdd[\x04\xc0yU\x04\x10\x00\x8bRl\x11\x00\
\x9b\xdf\xbdG\x00,@\xcf\x00\x01\xb0\xf0<\x0b\x04\xc0\
\xf9S\x04\x10\x00\x8b\xcc\xf3A\x00,.\x13\x1a\x02`\
\xf3{f\x08\x80\x85\xe4\xd9\x09\x00FI\x11\x10\x00,\
\x1a1\x17\x00\x8b\x05\xcfV\x00,\x10<c\x01\xb00\
\xf0\xac\x05\xc0\xf9\x10\x11\x10\x00\x8b\x00\xf1\x17\x00\x9b\x1f\
kA\x00<p\xac\x09\x01\xf0\xa0\xb16\x04\xc0y\x0f\
\x11\x10\x00\x0f\x15\xeb\xa5\x1bw\x0f\x13r\xd7\x8d_\x04\
\x82`\x02\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\
\x00 \x00\x80\x00\x00\x02\xd0\xbeeY<e\xac\x9d\xe4\
\x09@\x04\xb0f\xc2\x8f\x00\xcb\xb2\x08\x016\x7f\xfa\xf7\
\x00D\x00o\x12\xc1\x01\x10\x01\xac\x89\xf0\x00\x88\x00\xd6\
Bx\x00D\x00k\xc0\xef\x01\xf8\xe6\xa0\xcd/\x00X\
\x0c\x9e\xb7\x00X\x14\x16\x85\x89O\x00,\x10<[\x01\
\xb0P\xf0L\x05\xc0\xa8\x88\xcd/\x00\x16\x0f\x9e\x9f\x00\
XD\x98\xe0\x04@\x04\xf0\xac\x04\xc0\xc2\xc23\x12\x00\
\xa3%6\xbf\x00Xlx\x1e\x02`\xd1a\x22\x13\x00\
\x11\xc0\xbd\x17\x00\x0b\x11\xf7\x5c\x00\x8c\xa2\xd8\xfc\x02`\
q\xe2\xfe\x0a\x80E\x8a\x09K\x00D\x00\xf7R\x00,\
\x5c\xdcC\x010\xbab\xf3\x0b\x80\xc5\x8c\xfb%\x00\x16\
\xb5\xfb\x84\x00X\xdc\x8eK\x08\x80\x08\xb8'\x08\x80w\
;\x9b\x1f\x01\xb0\xf8]?\x02`\x13\x98\x80\x10\x00\x11\
p\xad\x08\xc0\x08\x0b\xad\xe5\xd7\xfb\xa9\xd7f\xf3\x0b\x80\
\x05s\xe2h\x9c\x10\x11\xaf9l\x02x\xe7!\xf4\xfe\
\xe0Z\xbc\xe6Q\xc3t\xd6k\xef\xe1\x9a\xeb\x88\x0fa\
\x94j\xb7x\xcd{\xbfN\xea7\xfbz\xb9\xe6:\xd2\
M\x1dq\xb1m\xb9\xa6\xab\xafy\xeb\xd7\x1b\xe9Y\xec\
Y[=]w\xf9\xf9\x07\xd34=\x94\x91\xad\xe6y\
\x8e|\xc6g]\xf7\xb3\xbf\xf7\x93\xd6u\xfdg\xcf\xdf\
-a\x04=\xf7\xba\xfd\x1e\x00\x04\x13\x00\x10\x00@\x00\
\x00\x01\x00\x04\x00\x10\x00@\x00\x00\x01\x00\x04\x00\x10\x00\
@\x00\x00\x01\x00\x04\x00\x10\x00@\x00\x00\x01\x00\x04\x00\
\x10\x00@\x00\x00\x01\x00\x04\x00\x18=\x00g\xff\x7f\xcf\
\xa1W\xdf\xd8\x1b5\xe5B\xc1\xe6o\xe8\x08 \x02\xf0\
\xfd\xbdPS/\x1c\xd27\xff\xed\xf6\xe4\xb3\x01o\xb7\
\xf3?\x1f\xf0\x15\x9f\x1b\x88M\x7f\xae\xa6?\x1b\xd0D\
0V<=\xcf\xf6\xf91\xe0@\xef*6\x1c\x02 \
\x04n\x02\x9b\xc6\xff\x97\x01x\xf6\x1f\x22\x02\x98\x00p\
$ !\x00\xa6\x00\xd3\x00c\x8f\xff&\x00\x11 \xdc\
\x7f\xdf\xe5\xbf\xf5;\x01\x9c\xe3\xca\x1f\x17\x0aO\xdb\xef\
\xfe\x9b&\x00G\x01\xd3\x00cn~G\x00\x11\xc0\x11\
`\x1bG\x01G\x02\xa1\x19\xeb\xdd\x7f\xd7\x04\xe0(`\
\x1a`\xac\xcd\xbf\xfb\x08 \x02\x22\xc08\x9b\x7f\xd7\x11\
\xc0q\xc0\x91@X\xc6\xda\xfc\xbb'\x00\x93\x80i\x80\
q6\xff\xdb\x01\x10\x01\x11\xa0\xff\xcd\xff\xf6\x11\xc0q\
\xc0\x91@H\xfa\xdf\xfc\x87&\x00\x93\x80i\x80\xbe7\
\xffG&\x00\xd3\x80I@<\xfa\xdb\xf8\xa7\x04@\x08\
\x84@\x00\xfa\xd8\xf8\xa7\x06@\x08D@\x00\xda\xde\xf8\
\x97\x04@\x08\xb2# \x00\xedn\xfcK\x03 \x08\x99\
\x11\x10\x80\xf66|\x13\x01`\xach\xbe\x0a\xc1\xd9\x01\
\xf0\x13\xa8\xe3\xfcs`\xef\x0c\x87y\xa7\x17\x00D@\
\x08\x04\x80+\x22`\x1a@\x00\x84@\x048\xcc7Q\
:\x97\xfc\x13\x15\xdf\x044\x01\x98\x04l\x02L\x00$\
N\x03\xe2g\x02\xc0\x86@\x00\x10\x01\x1c\x01\x88:\x12\
\x88\x9d\x09\x00\x1b\x04\x01@\x04p\x04 \xeaH n\
&\x00l\x18\x04\x00\x11\xc0\x11\x80\xa8#\x81\x98\x99\x00\
\xb0\x81\x10\x00D\x00G\x00\xa2\x8e\x04\xe2e\x02\xc0\x86\
B\x00\x10\x01\x1c\x01\x88:\x12\x88\x95\x09\x00\x1b\x0c\x01\
@\x04p\x04 \xeaH N&\x00l8\x04\x00\x11\
\xc0\x11\x80\xa8#\x81\x18\x99\x00\xb0\x01\x11\x00D\x00G\
\x00\xa2\x8e\x04\xe2c\x02\xc04\x80\x09\x80\xc4i@p\
L\x00\x98\x06\x10\x00D\x00\x01@\x04\xf0=\x002\xbe\
/ .&\x00L\x03\x08\x00\x22\x80#\x00QG\x02\
11\x01`\x1a@\x00\x10\x01\x1c\x01\x88:\x12\x88\x87\
\x00\x10\x1a\x01\x9b_\x00\x08\x8d\x80\xcd\x0fA1\xe8\xfd\
S\x8c\x01\x00\xda\xf2\x07\xc0\xb4\x09d\x9d\x1fRw\x00\
\x00\x00\x00IEND\xaeB`\x82\
"
qt_resource_name = b"\
\x00\x08\
\x0aaZ\xa7\
\x00i\
\x00c\x00o\x00n\x00.\x00p\x00n\x00g\
"
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x847\x9eu\x9f\
"
def qInitResources():
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
qInitResources()

232
manim_slides/slide.py Normal file
View File

@ -0,0 +1,232 @@
import os
import platform
import shutil
import subprocess
from typing import Any, List, Optional
from tqdm import tqdm
from .config import PresentationConfig, SlideConfig, SlideType
from .defaults import FOLDER_PATH
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
def reverse_video_file(src: str, dst: str) -> None:
"""Reverses a video file, writting the result to `dst`."""
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
logger.debug(" ".join(command))
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
if output:
logger.debug(output.decode())
if error:
logger.debug(error.decode())
class Slide(Scene): # type:ignore
"""
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
"""
def __init__(
self, *args: Any, output_folder: str = FOLDER_PATH, **kwargs: Any
) -> None:
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[SlideConfig] = []
self.current_slide = 1
self.current_animation = 0
self.loop_start_animation: Optional[int] = 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) -> bool:
"""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: Any, **kwargs: Any) -> None:
"""Overloads `self.play` and increment animation count."""
super().play(*args, **kwargs)
self.current_animation += 1
def pause(self) -> None:
"""Creates a new slide with previous animations."""
self.slides.append(
SlideConfig(
type=SlideType.slide,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
self.current_slide += 1
self.pause_start_animation = self.current_animation
def add_last_slide(self) -> None:
"""Adds a 'last' slide to the end of slides."""
self.slides.append(
SlideConfig(
type=SlideType.last,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
def start_loop(self) -> None:
"""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) -> None:
"""Ends an existing loop."""
assert (
self.loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
SlideConfig(
type=SlideType.loop,
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
self.current_slide += 1
self.loop_start_animation = None
self.pause_start_animation = self.current_animation
def save_slides(self, use_cache: bool = True) -> None:
"""
Saves slides, optionally using cached files.
Note that cached files only work with Manim.
"""
self.add_last_slide()
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
files_folder = os.path.join(self.output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
scene_name = type(self).__name__
scene_files_folder = os.path.join(files_folder, scene_name)
old_animation_files = set()
if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder)
elif not use_cache:
shutil.rmtree(scene_files_folder)
os.mkdir(scene_files_folder)
else:
old_animation_files.update(os.listdir(scene_files_folder))
files = []
for src_file in tqdm(
self.partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=self.leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self.show_progress_bar,
):
filename = os.path.basename(src_file)
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
if filename in old_animation_files:
old_animation_files.remove(filename)
else:
shutil.copyfile(src_file, dst_file)
# We only reverse video if it was not present
if rev_filename in old_animation_files:
old_animation_files.remove(rev_filename)
else:
rev_file = os.path.join(scene_files_folder, rev_filename)
reverse_video_file(src_file, rev_file)
files.append(dst_file)
logger.info(
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
)
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
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: Any, **kwargs: Any) -> None:
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
def render(self, *args: Any, **kwargs: Any) -> None:
"""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): # type: ignore
"""
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

196
manim_slides/wizard.py Normal file
View File

@ -0,0 +1,196 @@
import os
import sys
from functools import partial
from typing import Any
import click
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QKeyEvent
from PySide6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QGridLayout,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from .commons import config_options, verbosity_option
from .config import Config, Key
from .defaults import CONFIG_PATH
from .manim import logger
from .resources import * # noqa: F401, F403
WINDOW_NAME: str = "Configuration Wizard"
keymap = {}
for key in Qt.Key:
keymap[key.value] = key.name.partition("_")[2]
class KeyInput(QDialog): # type: ignore
def __init__(self) -> None:
super().__init__()
self.key = None
self.layout = QVBoxLayout()
self.setWindowTitle("Keyboard Input")
self.label = QLabel("Press any key to register it")
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def keyPressEvent(self, event: QKeyEvent) -> None:
self.key = event.key()
self.deleteLater()
event.accept()
class Wizard(QWidget): # type: ignore
def __init__(self, config: Config):
super().__init__()
self.setWindowTitle(WINDOW_NAME)
self.config = config
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
QBtn = QDialogButtonBox.Save | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.saveConfig)
self.buttonBox.rejected.connect(self.closeWithoutSaving)
self.buttons = []
self.layout = QGridLayout()
for i, (key, value) in enumerate(self.config.dict().items()):
# Create label for key name information
label = QLabel()
key_info = value["name"] or key
label.setText(key_info)
self.layout.addWidget(label, i, 0)
# Create button that will pop-up a dialog and ask to input a new key
value = value["ids"].pop()
button = QPushButton(keymap[value])
button.setToolTip(
f"Click to modify the key associated to action {key_info}"
)
self.buttons.append(button)
button.clicked.connect(
partial(self.openDialog, i, getattr(self.config, key))
)
self.layout.addWidget(button, i, 1)
self.layout.addWidget(self.buttonBox, len(self.buttons), 1)
self.setLayout(self.layout)
def closeWithoutSaving(self) -> None:
logger.debug("Closing configuration wizard without saving")
self.deleteLater()
sys.exit(0)
def closeEvent(self, event: Any) -> None:
self.closeWithoutSaving()
event.accept()
def saveConfig(self) -> None:
try:
Config.parse_obj(self.config.dict())
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Error")
msg.setInformativeText(
"Two or more actions share a common key: make sure actions have distinct key codes."
)
msg.setWindowTitle("Error: duplicated keys")
msg.exec_()
return
self.deleteLater()
def openDialog(self, button_number: int, key: Key) -> None:
button = self.buttons[button_number]
dialog = KeyInput()
dialog.exec_()
if dialog.key is not None:
key_name = keymap[dialog.key]
key.set_ids(dialog.key)
button.setText(key_name)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: str, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Initialize a new default configuration file."""
return _init(config_path, force, merge, skip_interactive=True)
def _init(
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""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")
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 not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if os.path.exists(config_path):
config = Config.parse_file(config_path)
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
config = window.config
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=2))
click.secho(f"Configuration file successfully saved to `{config_path}`")

2576
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

68
pyproject.toml Normal file
View File

@ -0,0 +1,68 @@
[tool.poetry]
name = "manim-slides"
description = "Tool for live presentations using manim"
authors = [
"Jérome Eertmans <jeertmans@icloud.com>"
]
version = "4.8.0"
license = "GPL-3.0-only"
readme = "README.md"
homepage = "https://github.com/jeertmans/manim-slides"
documentation = "https://eertmans.be/manim-slides"
repository = "https://github.com/jeertmans/manim-slides"
keywords = ["manim", "slides", "plugin", "manimgl"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering",
]
exclude = ["docs/","static/"]
packages = [
{ include = "manim_slides" },
]
[tool.poetry.dependencies]
python = ">=3.8.1,<3.12"
click = ">=8.0"
click-default-group = ">=1.2"
numpy = ">=1.19"
opencv-python = ">=4.6"
pydantic = ">=1.9"
pyside6 = ">=6"
requests = ">=2.26"
tqdm = ">=4.62"
[tool.poetry.group.docs.dependencies]
manim = "^0.17.0"
sphinx = "^5.3.0"
sphinx-click = "^4.4.0"
furo = "^2022.9.29"
[tool.poetry.group.dev.dependencies]
black = "^22.10.0"
mypy = "^0.991"
isort = "^5.10.1"
flake8 = "^6.0.0"
[tool.poetry.group.test.dependencies]
manim = "^0.17.0"
manimgl = "^1.6.1"
[build-system]
requires = ["setuptools","poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.plugins]
[tool.poetry.plugins."console_scripts"]
manim-slides = "manim_slides.__main__:cli"

View File

@ -1,28 +0,0 @@
import setuptools
import os
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "README.md"), "r") as fh:
long_description = fh.read()
setuptools.setup(
name="manim_presentation",
version="0.1.2",
author="Federico A. Galatolo",
author_email="federico.galatolo@ing.unipi.it",
description="Tool for live presentations using manim",
url="https://github.com/galatolofederico/manim-presentation",
long_description=long_description,
long_description_content_type="text/markdown",
packages=setuptools.find_packages(),
entry_points = {
"console_scripts": ["manim_presentation=manim_presentation.present:main"],
},
install_requires=[
],
classifiers=[
"Programming Language :: Python :: 3.7",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Development Status :: 4 - Beta"
],
)

BIN
static/docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
static/example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

BIN
static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

41
static/logo.py Normal file
View File

@ -0,0 +1,41 @@
# flake8: noqa: F403, F405
# type: ignore
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