Compare commits

...

70 Commits

Author SHA1 Message Date
6a96b3ab8c chore(version): bump 4.13.1 to 4.13.2 2023-05-25 17:47:50 +02:00
a1c041db80 chore(paper): add comparison section (#192)
Closes #188
2023-05-24 09:04:05 +02:00
4fd3452f95 chore(cli): fix and improve help messages (#191)
A very small PR to fix an error in an help message, and improve the verbosity one.
2023-05-23 10:42:23 +02:00
ff2be6851b [pre-commit.ci] pre-commit autoupdate (#189)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.267 → v0.0.269](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.267...v0.0.269)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-23 10:41:57 +02:00
95289ee7a5 [pre-commit.ci] pre-commit autoupdate (#187)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.8.0 → v2.9.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.8.0...v2.9.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.265 → v0.0.267](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.265...v0.0.267)
- [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-18 17:19:31 +02:00
f1a026208a chore(docs): document scene subclassing (#186) 2023-05-11 19:43:32 +02:00
b3fd1d209e [pre-commit.ci] pre-commit autoupdate (#184)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.263 → v0.0.265](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.263...v0.0.265)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-09 10:27:21 +02:00
8c38db0989 chore(convert): add debug message 2023-05-08 19:15:31 +02:00
6da0c36c96 Revert "try(ci): remove cache for media"
This reverts commit 3b01efa6018dfb0902cfcd92e713ac8dd8f70e67.
2023-05-08 19:10:48 +02:00
3b01efa601 try(ci): remove cache for media 2023-05-08 18:39:14 +02:00
c9ef5e9a75 fix(pages): missing assets (#183)
* fix(pages): missing assets

This is a PR to try understanding why some assets are not present

* fix(ci): correct path

* fix(ci): fix path..

* chore(ci): add debug

* chore(ci): more and more debug
2023-05-08 17:43:58 +02:00
bfad43bd38 chore(version): bump 4.13.0 to 4.13.1 2023-05-08 10:19:30 +02:00
6f2cbc9b19 fix(convert): --use-template fixed (#182)
As described in #181, there was a mismatch between the type return by `click` and the one used by `pydantic`. Now we only use `Path` types.

Closes #181
2023-05-08 10:18:57 +02:00
5bd88c2fd5 chore(version): bump 4.12.0 to 4.13.0 2023-05-07 23:24:01 +02:00
f0c17b1e2a chore(paper): update statement of need (#176)
* chore(paper): update statement of need

Closes #171

* chore(paper): making link more time-proof
2023-05-07 23:22:47 +02:00
fce9546a9b chore(ci/deps): publish wheels and add manim/manimgl as extras (#173)
* chore(deps): add manim and manimgl as extras

* chore(ci): publish wheels too

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

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

* chore(ci): run tests on dep. changes

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

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

* chore(ci): only use extras to build pages

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-07 23:01:59 +02:00
d6ad56120e chore(docs): update contributing guidelines (#177)
Add seeking for help and reporting an issue sections.

Closes #172
2023-05-07 23:01:44 +02:00
5db0261b01 chore(docs): update install documentation (#175)
* chore(docs): update install documentation

Closes #169

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

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

* chore(docs): update according to code quality report

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-07 23:01:32 +02:00
8ab33ef71f feat(cli): add more debugging messages (#180)
* feat(cli): add more debugging messages

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

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

* chore(lib): fix msg to be more correct

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

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

* chore(version): setup dummy version


chore(version): fix

* chore(ci): udpate version in __version__ too

* chore(version): revert changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-04 10:54:24 +02:00
4da0e2cc2d [pre-commit.ci] pre-commit autoupdate (#178)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.262 → v0.0.263](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.262...v0.0.263)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-02 09:55:05 +02:00
0e82e28313 chore(ci): add action dependabot checks (#174)
Setup dependabot to check for new actions
2023-05-01 11:52:22 +02:00
8b13106fcc chore(paper): suggestions from JOSE review (#168)
* Suggestions for paper

* A  few suggestions for the documentation
2023-05-01 11:06:55 +02:00
bce4d8188f [pre-commit.ci] pre-commit autoupdate (#167)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.260 → v0.0.262](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.260...v0.0.262)
- [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-27 00:00:14 +02:00
c420b47ad2 chore(version): bump 4.11.0 to 4.12.0 2023-04-07 10:42:57 +02:00
fad13f33dc [pre-commit.ci] pre-commit autoupdate (#166)
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.257 → v0.0.260](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.257...v0.0.260)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-07 10:40:04 +02:00
d42a7f5ff1 [pre-commit.ci] pre-commit autoupdate (#164)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.7.0 → v2.8.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.7.0...v2.8.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.255 → v0.0.257](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.255...v0.0.257)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-23 22:59:22 +01:00
88d598709a feat(cli/lib): use scene background color as default (#160)
* Use scene background color as default

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

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

* Minor changes to feature: Read scene background

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

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

* Small fix for feature "Read bg color"

* chore(ci): add typing ignore

* fix(ci): typo

---------

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>
2023-03-22 13:59:04 +01:00
2ba9b734a3 chore(version): bump 4.10.0 to 4.11.0 2023-03-20 16:32:24 +01:00
49c4a10453 fix(lib): prevent calling child method (#163)
Change `pause` to not call eventual child's `next_slide` method
2023-03-20 16:24:46 +01:00
8c578d2577 fix(cli): do not terminate slides early (#162)
* fix(cli): do not terminate slides early

When a slide is replayed (either normally or reversed), its state must be reset.

Closes #161

* [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>
2023-03-20 16:24:36 +01:00
2a327c470b feat(cli): auto detect resolution (#158)
* feat(cli): auto detect resolution

The `present` command will now read by default the resolution of each presentation, and only change it if specified by the user.

This PR also fixes bugs introduced by #156 and previous PRs, where the transition between two presentation was not correct...

* fix(lib): better to test if not None
2023-03-16 15:41:31 +01:00
04dcf530f5 [pre-commit.ci] pre-commit autoupdate (#157)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.254 → v0.0.255](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.254...v0.0.255)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-14 14:48:13 +01:00
9a573f29f1 feat(cli): add start at index options (#156)
* wip: start at index

* feat(cli): add start at index options

* fix(ci): correct callback

* chore(ci): fix type hint

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

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

* fix(cli): typo in variable name...

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-11 18:05:07 +01:00
02f425f536 feat(render): support skipping animations (#155)
* feat(render): support skipping animations

Manim Slides now supports when Manim or ManimGL skip rendering some animations. All non-public attributes or methods are now named with leading __

* [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>
2023-03-10 17:49:37 +01:00
149b12fd01 chore(lib): raise error if skipping
Temporary error before implementing this
2023-03-09 17:29:26 +01:00
e01be300a0 chore(version): bump 4.9.2 to 4.10.0 2023-03-09 14:16:43 +01:00
940916d4aa chore(lib): some fixes before new release 2023-03-09 14:06:02 +01:00
3da8fab145 chore(deps): remove pkg_resources in favor to importlib (#153)
* feat(convert): PowerPoint conversion 

You can now convert your presentations to PowerPoint

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

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

* chore(convert): autoplay media

* chore(deps): remove pkg_resources in favor to importlib

This is what pkg_resources' team recommends

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

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

* fix(deps): if case for Python 3.8 (<3.9)

* fix(convert): use correct pkg path

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

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

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

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

* fix(deps): remove duplicate deps

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

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

* fix(lib): add __init__.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-09 13:41:25 +01:00
f0c5d48107 feat(convert): PowerPoint conversion (#152)
* feat(convert): PowerPoint conversion 

You can now convert your presentations to PowerPoint

* fix(deps): push poetry lock

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

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

* fix(lint): ignore attr-defined because Windows-only

* chore(convert): autoplay media

* fix(convert): autoplay and autoloop

* chore(deps): update lock file

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

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

* feat(convert): use first frame as default poster frame

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

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

* chore(docs): document new feature

* [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>
2023-03-09 12:12:45 +01:00
426470ef3c chore(lib): use next_slide not pause (#151)
* chore(lib): use `next_slide` not `pause`

This deprecates `pause` function in favor to `next_slide`, that will also be called by `next_section` in the future.

* [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>
2023-03-08 16:57:10 +01:00
700584cbcc chore(lib): reduce import overhead (#147)
* chore(lib): reduce import overhead

This PR should reduce the import time overhead caused by manim imports. To solve this, manim is only imported when Slide or ThreeDSlide is needed. A custom logger is now defined, which copies the one from Manim Community. FFMPEG_BIN is now hardcoded, but should be configurable in the future via the CLI or some config file.

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

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

* fix(lib): remove last .manim import

* fix(lib): remove print

* chore(lib): fix typo

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-08 16:56:51 +01:00
a440da9468 [pre-commit.ci] pre-commit autoupdate (#148)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.253 → v0.0.254](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.253...v0.0.254)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-07 11:53:09 +01:00
6486ce147c fix(docs): small typo 2023-03-04 10:12:35 +01:00
b258deeb31 fix(style): no backstick 2023-03-03 23:01:02 +01:00
a32773c50f chore(docs): adding GUI and HTML documentation pages (#145)
* chore(docs): adding GUI and HTML documentation pages

As titled, this adds two pages to the docs

* fix(typo): languagetool suggestion
2023-03-03 21:56:00 +01:00
a16aa93ee6 chore(misc): JOSE paper (#144)
* chore(misc): JOSE paper

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

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

* fix(ci): update path

* fix(ci): tab error

* fix(ci): path to paper

* fix(paper): updates

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

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

* fix(paper): typos

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-03 16:34:35 +01:00
e809e64f9a fix(docs): typo in commands 2023-03-02 13:36:14 +01:00
5967760dc3 feat(cli): using cached files when possible (#142)
* feat(cli): using cached files when possible

This should improve a bit the overall performances

* [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>
2023-03-02 13:33:58 +01:00
7f824be682 chore(README): force links in badges 2023-03-02 13:32:57 +01:00
9346f199d7 chore(README): add docs badge and update wizard (#143) 2023-03-02 13:24:58 +01:00
5c40dc69d8 chore(docs): transparent logo and symbolic link (#141) 2023-03-02 12:47:42 +01:00
bf10068cfc [pre-commit.ci] pre-commit autoupdate (#140)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.249 → v0.0.253](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.249...v0.0.253)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-28 08:02:49 +01:00
2f307225d1 chore(ci): clear cache (#138)
* chore(ci): clear cache

We must force cache clearing to save a new media/ directory

* chore(ci): clear cache when PR is closed

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

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

* fix(ci): add write permission

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-27 11:24:50 +01:00
8b5db4b2fd chore(docs): add dark-themed logo (#137)
* chore(docs): add dark-themed logo

* fix(docs): swap themes
2023-02-26 12:29:43 +01:00
855c74de34 chore(version): bump 4.9.1 to 4.9.2 2023-02-26 00:17:12 +01:00
a70876d696 fix(convert): relative path in HTML files (#136)
This fixes an error introduced by #133
2023-02-26 00:11:12 +01:00
3cb0085f24 chore(version): bump 4.9.0 to 4.9.1 2023-02-25 17:41:09 +01:00
42d70380b0 chore(docs): add embed instructions (#135) 2023-02-25 17:29:42 +01:00
dc1be25e6e chore(lib): use pathlib.Path instead of str (#133)
* wip(lib): change os.path to pathlib.Path

* chore(lib): use pathlib.Path instead of str

* fix(logger): convert Path to str

* chore(lint): add type hint to prevent future errors

* fix(lib): correct suffix addition
2023-02-25 17:21:50 +01:00
4cd433b35a chore(docs): document sharing slides (#134)
* chore(docs): remove deprecated "last animation" mention

* chore(docs): document sharing slides
2023-02-25 13:30:12 +01:00
e83df48c5d chore(version): bump 4.8.4 to 4.9.0 2023-02-25 11:37:17 +01:00
ed30e2136a Add Feature BackgroundSize (#132) 2023-02-24 17:51:33 +01:00
a9f5355595 chore(deps): bump ipython from 8.9.0 to 8.10.0 (#126)
Bumps [ipython](https://github.com/ipython/ipython) from 8.9.0 to 8.10.0.
- [Release notes](https://github.com/ipython/ipython/releases)
- [Commits](https://github.com/ipython/ipython/compare/8.9.0...8.10.0)

---
updated-dependencies:
- dependency-name: ipython
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-24 12:44:49 +01:00
1ef42ec82a chore(deps): bump markdown-it-py from 2.1.0 to 2.2.0 (#129)
Bumps [markdown-it-py](https://github.com/executablebooks/markdown-it-py) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/executablebooks/markdown-it-py/releases)
- [Changelog](https://github.com/executablebooks/markdown-it-py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/markdown-it-py/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: markdown-it-py
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-24 09:04:56 +01:00
b5f6a165db [pre-commit.ci] pre-commit autoupdate (#127)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.6.0 → v2.7.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.6.0...v2.7.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.243 → v0.0.249](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.243...v0.0.249)
- [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-21 08:38:54 +01:00
7b9c9b0c39 fix(lib): a class can now have zero slide and work (#125)
* fix(lib): a class can now have zero slide and work

This fixes a previous issue that occured when a class didn't have any `pause`

* [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>
2023-02-08 18:58:54 +01:00
ac23949043 chore(version): bump 4.8.3 to 4.8.4 2023-02-08 11:01:38 +01:00
71564a4c2e fix(convert): use hash to restrict the length of new filenames (#124)
Closes #123
2023-02-08 11:00:53 +01:00
b06250056d [pre-commit.ci] pre-commit autoupdate (#121)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.237 → v0.0.243](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.237...v0.0.243)

* [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>
2023-02-08 09:32:03 +01:00
43c24d7ae1 fix(docs): update base site url 2023-02-01 11:47:02 +01:00
55 changed files with 2582 additions and 997 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.8.3
current_version = 4.13.2
commit = True
message = chore(version): bump {current_version} to {new_version}

13
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
labels:
- dependencies

34
.github/workflows/clearcache.yml vendored Normal file
View File

@ -0,0 +1,34 @@
# From: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup caches by a branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

33
.github/workflows/draft-pdf.yml vendored Normal file
View File

@ -0,0 +1,33 @@
# Simple workflow for deploying static content to GitHub Pages
name: Create JOSE Paper
on:
# Runs on pushes targeting the default branch
push:
paths:
- paper/*
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
paper:
runs-on: ubuntu-latest
name: Paper Draft
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build draft PDF
uses: openjournals/openjournals-draft-action@master
with:
journal: jose
# This should be the path to the paper within your repo.
paper-path: paper/paper.md
- name: Upload
uses: actions/upload-artifact@v1
with:
name: paper
# This is the output path where Pandoc will write the compiled
# PDF. Note, this should be the same directory as the input
# paper.md
path: paper/paper.pdf

View File

@ -25,6 +25,7 @@ concurrency:
jobs:
# Single deploy job since we're just deploying
deploy:
permissions: write-all
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
@ -45,22 +46,30 @@ jobs:
run: |
sudo apt-get update
sudo apt-get 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
run: poetry install --extras=manim --with docs
- name: Restore cached media
id: cache-media-restore
uses: actions/cache/restore@v3
with:
path: media
key: ${{ runner.os }}-media
- name: Build animation and convert it into HTML slides
- name: Build animations
run: |
poetry run manim example.py ConvertExample BasicExample ThreeDExample
poetry run manim-slides convert ConvertExample docs/source/_static/slides.html -ccontrols=true
poetry run manim-slides convert BasicExample docs/source/_static/basic_example.html -ccontrols=true
poetry run manim-slides convert ThreeDExample docs/source/_static/three_d_example.html -ccontrols=true
- name: Convert animations to HTML slides
run: |
poetry run manim-slides convert -v DEBUG ConvertExample docs/source/_static/slides.html -ccontrols=true
poetry run manim-slides convert -v DEBUG BasicExample docs/source/_static/basic_example.html -ccontrols=true
poetry run manim-slides convert -v DEBUG ThreeDExample docs/source/_static/three_d_example.html -ccontrols=true
- name: Show docs/source/_static/ dir content (video only)
run: tree -L 3 docs/source/_static/ -P '*.mp4'
- name: Clear cache
run: |
gh extension install actions/gh-actions-cache
gh actions-cache delete ${{ steps.cache-media-restore.outputs.cache-primary-key }} --confirm || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save media to cache
id: cache-media-save
uses: actions/cache/save@v3
@ -75,6 +84,8 @@ jobs:
with:
# Upload docs/build/html dir
path: docs/build/html/
- name: Show docs/build/html/_static/ dir content (video only)
run: tree -L 3 docs/build/html/_static/ -P '*.mp4'
- name: Deploy to GitHub Pages
id: deployment
if: github.event_name != 'pull_request'

View File

@ -1,4 +1,3 @@
# Modified from: https://github.com/pypa/cibuildwheel
name: Upload Python Package
on:
@ -8,41 +7,28 @@ on:
types: [published]
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
build_and_release:
name: Build and release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v3
- uses: actions/setup-python@v2
- name: Install Poetry
run: pipx install poetry
- name: Install build package
run: python -m pip install -U build
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: poetry
- name: Build wheels
run: python -m build --sdist
run: poetry build
- 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 }}
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
run: poetry publish

View File

@ -1,6 +1,8 @@
on:
pull_request:
paths:
- pyproject.toml
- poetry.lock
- '**.py'
- .github/workflows/test_examples.yml
workflow_dispatch:

6
.gitignore vendored
View File

@ -35,3 +35,9 @@ docs/source/_static/basic_example.html
docs/source/_static/three_d_example.html
docs/source/_static/three_d_example_assets/
paper/media/
*.jats
paper/paper.pdf

View File

@ -12,7 +12,7 @@ repos:
- id: isort
name: isort (python)
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.6.0
rev: v2.9.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
@ -20,15 +20,15 @@ repos:
exclude: poetry.lock
args: [--autofix]
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.237
rev: v0.0.269
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
rev: v1.3.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

View File

@ -1,13 +1,18 @@
![Manim Slides Logo](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
</picture>
[![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)
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
[![Documentation][documentation-badge]][documentation-url]
# Manim Slides
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!
> **NOTE:** this project extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
- [Installation](#installation)
* [Dependencies](#dependencies)
@ -22,6 +27,9 @@ Tool for live presentations using either [Manim (community edition)](https://www
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
- [Contributing](#contributing)
* [Reporting an Issue](#reporting-an-issue)
* [Seeking for Help](#seeking-for-help)
* [Contact](#contact)
## Installation
@ -49,6 +57,16 @@ The recommended way to install the latest release is to use pip:
pip install manim-slides
```
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
pip install manim-slides[manim] # For Manim
# or
pip install manim-slides[manimgl] # For ManimGL
```
[^1]: NOTE: you still need to have Manim or ManimGL platform-specific dependencies installed on your computer.
### Install From Repository
An alternative way to install Manim Slides is to clone the git repository, and install from there: read the [contributing guide](https://eertmans.be/manim-slides/contributing/workflow.html) to know how.
@ -60,7 +78,7 @@ An alternative way to install Manim Slides is to clone the git repository, and i
<!-- start usage -->
Using Manim Slides is a two-step process:
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.pause()` everytime you want to create a new slide.
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.next_slide()` everytime you want to create a new slide.
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
The documentation is available [online](https://eertmans.be/manim-slides/).
@ -82,14 +100,14 @@ class BasicExample(Slide):
dot = Dot()
self.play(GrowFromCenter(circle))
self.pause() # Waits user to press continue to go to the next slide
self.next_slide() # Waits user to press continue to go to the next slide
self.start_loop() # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop() # This will loop until user inputs a key
self.play(dot.animate.move_to(ORIGIN))
self.pause() # Waits user to press continue to go to the next slide
self.next_slide() # Waits user to press continue to go to the next slide
```
First, render the animation files:
@ -118,7 +136,11 @@ manim-slides BasicExample
The default key bindings to control the presentation are:
![manim-wizard](https://user-images.githubusercontent.com/27275099/197468787-19c83a81-d757-47b9-8f68-218427d30298.png)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/wizard_dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/wizard_light.png">
<img alt="Manim Slides Wizard" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/wizard_light.png">
</picture>
You can run the **configuration wizard** to change those key bindings:
@ -181,6 +203,58 @@ in *Settings*->*Display*.
Contributions are more than welcome! Please read through [our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
### Reporting an Issue
<!-- start reporting-an-issue -->
If you think you found a bug,
an error in the documentation,
or wish there was some feature that is currently missing,
we would love to hear from you!
The best way to reach us is via the
[GitHub issues](https://github.com/jeertmans/manim-slides/issues).
If your problem is not covered by an already existing (closed or open) issue,
then we suggest you create a
[new issue](https://github.com/jeertmans/manim-slides/issues/new/choose).
You can choose from a list of templates, or open a
[blank issue](https://github.com/jeertmans/manim-slides/issues/new)
if your issue does not fit one of the proposed topics.
The more precise you are in the description of your problem, the faster we will
be able to help you!
<!-- end reporting-an-issue -->
### Seeking for help
<!-- start seeking-for-help -->
Sometimes, you may have a question about Manim Slides,
not necessarily an issue.
There are two ways you can reach us for questions:
- via the `Question/Help/Support` topic when
[choosing an issue template](https://github.com/jeertmans/manim-slides/issues/new/choose);
- or via
[GitHub discussions](https://github.com/jeertmans/manim-slides/discussions).
<!-- end seeking-for-help -->
### Contact
<!-- start contact -->
Finally, if you do not have any GitHub account,
or just wish to contact the author of Manim Slides,
you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
<!-- end contact -->
[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
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
[documentation-badge]: https://img.shields.io/website?down_color=lightgrey&down_message=offline&label=documentation&up_color=green&up_message=online&url=https%3A%2F%2Feertmans.be%2Fmanim-slides%2F
[documentation-url]: https://eertmans.be/manim-slides/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 24 B

View File

@ -0,0 +1 @@
../../../static/logo.png

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 24 B

View File

@ -0,0 +1 @@
../../../static/logo_dark_docs.png

View File

@ -0,0 +1 @@
../../../static/logo_dark_github.png

View File

@ -0,0 +1 @@
../../../static/logo_dark_transparent.png

View File

@ -0,0 +1 @@
../../../static/logo_light_transparent.png

View File

@ -0,0 +1 @@
../../../static/wizard_dark.png

View File

@ -0,0 +1 @@
../../../static/wizard_light.png

View File

@ -24,6 +24,11 @@ extensions = [
"sphinx_copybutton",
]
myst_enable_extensions = [
"colon_fence",
"html_admonition",
]
templates_path = ["_templates"]
exclude_patterns = []
@ -35,6 +40,8 @@ html_theme = "furo"
html_static_path = ["_static"]
html_theme_options = {
"light_logo": "logo_light_transparent.png",
"dark_logo": "logo_dark_transparent.png",
"footer_icons": [
{
"name": "GitHub",
@ -62,4 +69,5 @@ intersphinx_mapping = {
# -- OpenGraph settings
ogp_site_url = "https://eertmans.be/manim-slides/"
ogp_use_first_image = True

View File

@ -2,10 +2,14 @@
Thank you for your interest in Manim Slides! ✨
Manim Slides is an open source project, first created as a fork of [manim-presentation](https://github.com/galatolofederico/manim-presentation) (now deprecated in favor to Manim Slides), and we welcome contributions of all forms.
This section is here to help fist-time contributors know how they can help this project grow. Whether you are already familiar with Manim or GitHub, it is worth taking a few minutes to read those documents!
Manim Slides is an open source project, first created as a fork of
[manim-presentation](https://github.com/galatolofederico/manim-presentation)
(now deprecated in favor to Manim Slides),
and we welcome contributions of all forms.
This section is here to help fist-time contributors know how they can help this
project grow. Whether you are already familiar with Manim or GitHub,
it is worth taking a few minutes to read those documents!
```{toctree}
:hidden:
@ -19,3 +23,24 @@ internals
[Internals](./internals)
: how Manim Slides is built and how the various parts of it work.
## Reporting an Issue
```{include} ../../../README.md
:start-after: <!-- start reporting-an-issue -->
:end-before: <!-- end reporting-an-issue -->
```
## Seeking for Help
```{include} ../../../README.md
:start-after: <!-- start seeking-for-help -->
:end-before: <!-- end seeking-for-help -->
```
## Contact
```{include} ../../../README.md
:start-after: <!-- start contact -->
:end-before: <!-- end contact -->
```

View File

@ -11,7 +11,7 @@ This document is there to help you recreate a working environment for Manim Slid
## Forking the repository and cloning it locally
We used GitHub to host Manim Slides' repository, and we encourage contributors to use git.
We use GitHub to host Manim Slides' repository, and we encourage contributors to use git.
Useful links:
@ -30,6 +30,32 @@ With Poetry, installation becomes straightforward:
poetry install
```
This, however, only installs the minimal set of dependencies to run the package.
If you would like to install Manim or ManimGL, as documented in the [quickstart](../quickstart),
you can use the `--extras` option:
```bash
poetry install --extras manim # For Manim
# or
poetry install --extras manimgl # For ManimGL
```
Additionnally, Manim Slides comes with group dependencies for development purposes:
```bash
poetry install --with dev # For linters and formatters
# or
poetry install --with docs # To build the documentation locally
```
Another group is `test`, but it is only used for
[GitHub actions](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/test_examples.yml).
:::{note}
You can combine any number of groups or extras when installing the package locally.
:::
## Running commands
As modules were installed in a new Python environment, you cannot use them directly in the shell.
@ -42,7 +68,7 @@ poetry run manim-slides wizard
or enter a new shell that uses this new Python environment:
```
poetry run
poetry shell
manim-slides wizard
```

View File

@ -0,0 +1,27 @@
# Features Table
The following summarizes the different presentation features Manim Slides offers.
:::{table} Comparison of the different presentation methods.
:widths: auto
:align: center
| Feature / Constraint | [`present`](reference/cli.md) | [`convert --to=html`](reference/cli.md) | [`convert --to=pptx`](reference/cli.md) |
| :--- | :---: | :---: | :---: |
| Basic navigation through slides | Yes | Yes | Yes |
| Replay slide | Yes | No | No |
| Pause animation | Yes | No | No |
| Play slide in reverse | Yes | No | No |
| Slide count | Yes | Yes (optional) | Yes (optional) |
| Animation count | Yes | No | No |
| Needs Python with Manim Slides installed | Yes | No | No |
| Requires internet access | No | Yes | No |
| Auto. play slides | Yes | Yes | Yes |
| Loops support | Yes | Yes | Yes |
| Fully customizable | No | Yes (`--use-template` option) | No |
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1]
| Works cross-platforms | Yes | Yes | Partly[^1][^2] |
:::
[^1]: If you encounter a problem where slides do not automatically play or loops do not work, please [file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).
[^2]: PowerPoint online does not seem to support automatic playing of videos, so you need LibreOffice Impress on Linux platforms.

View File

@ -4,9 +4,18 @@ og:description: Manim Slides makes creating slides with Manim super easy!
---
```{eval-rst}
.. image:: _static/logo.png
.. image:: _static/logo_light_transparent.png
:width: 600px
:align: center
:class: only-light
:alt: Manim Slide logo
```
```{eval-rst}
.. image:: _static/logo_dark_transparent.png
:width: 600px
:align: center
:class: only-dark
:alt: Manim Slide logo
```
@ -30,6 +39,7 @@ Slide through the demo below to get a quick glimpse on what you can do with Mani
quickstart
reference/index
features_table
```
```{toctree}

View File

@ -1,12 +1,12 @@
# Application Programming Interface
Manim Slides' API is very limited: it simply consists in two classes, `Slide` and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
Manim Slides' API is very limited: it simply consists of two classes, `Slide` and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
Thefore, we only document here the methods we think the end-user will ever use, not the methods used internally when rendering.
```{eval-rst}
.. autoclass:: manim_slides.Slide
:members: start_loop, end_loop, pause, play
:members: start_loop, end_loop, pause, next_slide
.. autoclass:: manim_slides.ThreeDSlide
:members:

View File

@ -66,6 +66,56 @@ Example using 3D camera. As Manim and ManimGL handle 3D differently, definitions
:end-before: [manimgl-3d]
```
## Subclass Custom Scenes
For compatibility reasons, Manim Slides only provides subclasses for
`Scene` and `ThreeDScene`.
However, subclassing other scene classes is totally possible,
and very simple to do actually!
[For example](https://github.com/jeertmans/manim-slides/discussions/185),
you can subclass the `MovingCameraScene` class from `manim`
with the following code:
```{code-block} python
:linenos:
from manim import *
from manim_slides import Slide
class MovingCameraSlide(Slide, MovingCameraScene):
pass
```
And later use this class anywhere in your code:
```{code-block} python
:linenos:
class SubclassExample(MovingCameraSlide):
def construct(self):
eq1 = MathTex("x", "=", "1")
eq2 = MathTex("x", "=", "2")
self.play(Write(eq1))
self.next_slide()
self.play(
TransformMatchingTex(eq1, eq2),
self.camera.frame.animate.scale(0.5)
)
self.wait()
```
:::{note}
If you do not plan to reuse `MovingCameraSlide` more than once, then you can
directly write the `construct` method in the body of `MovingCameraSlide`.
:::
## Advanced Example
A more advanced example is `ConvertExample`, which is used as demo slide and tutorial.

View File

@ -0,0 +1,71 @@
# Graphical User Interface
Manim Slides' graphical user interface (GUI) is the *de facto* way to present slides.
If you do not specify one of the commands listed in the [CLI reference](./cli),
Manim Slides will use **present** by default, which launches a GUI window,
playing your scene(s) like so:
```bash
manim-slides [present] [SCENES]...
```
Some optional parameters can be specified and can be listed with:
```bash
manim-slides present --help
```
:::{note}
All the `SCENES` must be in the same folder (`--folder DIRECTORY`), which
defaults to `./slides`. If you rendered your animations without changing
directory, you should not worry about that :-)
:::
## Configuration File
It is possible to configure Manim Slides via a configuration file, even though
this feature is currently limited. You may initiliaze the default configuration
file with:
```bash
manim-slides init
```
:::{warning}
Note that, by default, Manim Slides will use default key bindings that are
platform-dependent. If you decide to overwrite those with a config file, you may
encounter some problems from platform to platform.
:::
## Configuring Key Bindings
If you wish to use other key bindings than the defaults, you can run the
configuration wizard with:
```bash
manim-slides wizard
```
A similar window to the image below will pop up and prompt to change keys.
```{eval-rst}
.. image:: ../_static/wizard_light.png
:width: 300px
:align: center
:class: only-light
:alt: Manim Slide Wizard
```
```{eval-rst}
.. image:: ../_static/wizard_dark.png
:width: 300px
:align: center
:class: only-dark
:alt: Manim Slide Wizard
```
:::{note}
Even though it is not currently supported through the GUI, you can select
multiple key binding for the same action by modifying the config file.
:::

View File

@ -0,0 +1,40 @@
# HTML Presentations
Manim Slides allows you to convert presentations into one HTML file, with
[RevealJS](https://revealjs.com/). This file can then be opened with any modern
web browser, allowing for a nice portability of your presentations.
As with every command with Manim Slides, converting slides' fragments into one
HTML file (and its assets) can be done in one command:
```bash
manim-slides convert [SCENES]... DEST
```
where `DEST` is the `.html` destination file.
## Configuring the Template
Many configuration options are available through the `-c<option>=<value>` syntax.
Most, if not all, RevealJS options should be available by default. If that is
not the case, please
[fill an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
on GitHub.
You can print the list of available options with:
```bash
manim-slides convert --show-config
```
## Using a Custom Template
The default template used for HTML conversion can be found on
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/data/revealjs_template.html)
or printed with the `--show-template` option.
If you wish to use another template, you can do so with the
`--use-template FILE` option.
## More about HTML Slides
You can read more about HTML slides in the [sharing](./sharing) section.

View File

@ -8,10 +8,21 @@ Automatically generated reference for Manim Slides.
api
cli
examples
gui
html
sharing
```
[Application Programming Interface](./api): list of classes and methods that may be useful to the end-user.
[Application Programming Interface](./api): list of classes and methods that may
be useful to the end-user.
[Command Line Interface](./cli): list of all commands available using Manim Slides' executable.
[Command Line Interface](./cli): list of all commands available using Manim
Slides' executable.
[Examples](./examples): curated list of examples and their output.
[Graphical User Interface](./gui): details about the main Manim Slide' feature.
[HTML Presenetation](./html): an alternative way of presenting your animations.
[Sharing](./sharing): how to share your presentation with others.

View File

@ -0,0 +1,163 @@
# Sharing your slides
Maybe one of the most important features is the ability to share your
presentation with other people, or even with yourself but on another computer!
There exists a variety of solutions, and all of them are exposed here.
We will go from the *most restrictive* method, to the least restrictive one.
If you need to present on a computer without prior knowledge on what will be
installed on it, please directly refer to the last sections.
> **NOTES:** in the next sections, we will assume your animations are described
in `example.py`, and you have one presentation called `BasicExample`.
## With Manim Slides installed on the target machine
If Manim Slides, Manim (or ManimGL), and their dependencies are installed, then
using `manim-slides present` allows for the best presentations, with the most
options available.
### Sharing your Python file(s)
The lightest way to share your presentation is with the Python files that
describe the slides.
If you have such files, you can recompile the animations locally, and use
`manim-slides present` for your presentation. You may want to copy / paste
you own `.manim-slides.json` config file, but it is **not recommended** if
you are sharing from one platform (e.g., Linux) to another (e.g., Windows) as
the key bindings might not be the same.
Example:
```bash
# If you use ManimGl, replace `manim` with `manimgl`
manim example.py BasicExample
# This or `manim-slides BasicExample` works since
# `present` is implied by default
manim-slides present BasicExample
```
### Sharing your animations files
If you do not want to recompile all the animations, you can simply share the
slides folder (defaults to `./slides`). Then, Manim Slides will be able to read
the animations from this folder and its subdirectories.
Example:
```bash
# Make sure that the slides directory is in the current
# working directory, or specify with `--folder <FOLDER>`
manim-slides present BasicExample
```
and the corresponding tree:
```
.
└── slides
   ├── BasicExample.json
   └── files
     └── BasicExample (files not shown)
```
## Without Manim Slides installed on the target machine
An alternative to `manim-slides present` is `manim-slides convert`.
Currently, HTML and PPTX conversion are available, but do not hesitate to propose
other formats by creating a
[Feature Request](https://github.com/jeertmans/manim-slides/issues/new/choose),
or directly proposing a
[Pull Request](https://github.com/jeertmans/manim-slides/compare).
A major advantage of HTML files is that they can be opened cross-platform,
granted one has a modern web browser (which is pretty standard).
### Sharing HTML and animation files
First, you need to create the HTML file and its assets directory.
Example:
```bash
manim-slides convert BasicExample basic_example.html
```
Then, you need to copy the HTML files and its assets directory to target location,
while keeping the relative path between the HTML and the assets the same. The
easiest solution is to compress both the file and the directory into one ZIP,
and to extract it to the desired location.
By default, the assets directory will be named after the main HTML file, using `{basename}_assets`.
Example:
```
.
├── basic_example_assets
│   ├── 1413466013_2261824125_223132457.mp4
│   ├── 1672018281_2145352439_3942561600.mp4
│   └── 1672018281_3136302242_2191168284.mp4
└── basic_example.html
```
Then, you can simply open the HTML file with any web browser application.
If you want to embed the presentation inside an HTML web page, a possibility is
to use an `iframe`:
```html
<div style="position:relative;padding-bottom:56.25%;">
<!-- 56.25 comes from aspect ratio of 16:9, change this accordingly -->
<iframe
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
frameborder="0"
width="100%"
height="100%"
allowfullscreen
allow="autoplay"
src="basic_example.html">
</iframe>
</div>
```
The additional code comes from
[this article](https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page)
and it there to preserve the original aspect ratio (16:9).
### Sharing ONE HTML file
A future feature, that will be available once
[#122](https://github.com/jeertmans/manim-slides/issues/122) is solved, will be
to include all animations as data URI encoded, within the HTML file itself.
### Over the internet
Finally, HTML conversion makes it convenient to play your presentation on a
remote server.
This is how your are able to watch all the examples on this website. If you want
to know how to share your slide with GitHub pages, see the
[workflow file](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/pages.yml).
> **WARNING:** keep in mind that playing large video files over the internet
can take some time, and *glitches* may occur between slide transitions for this
reason.
### With PowerPoint (*EXPERIMENTAL*)
A recent conversion feature is to the PowerPoint format, thanks to the `python-pptx` package. Even though it is fully working, it is still considered in an *EXPERIMENTAL* status because we do not exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
Basically, you can create a PowerPoint in a single command:
```bash
manim-slides convert --to=pptx BasicExample basic_example.pptx
```
All the videos and necessary files will be contained inside the `.pptx` file, so you can safely share it with anyone. By default, the `poster_frame_image`, i.e., what is displayed by PowerPoint when the video is not playing, is the first frame of each slide. This allows for smooth transitions.
In the future, we hope to provide more features to this format, so feel free to suggest new features too!

View File

@ -22,14 +22,51 @@ class BasicExample(Slide):
dot = Dot()
self.play(GrowFromCenter(circle))
self.pause() # Waits user to press continue to go to the next slide
self.next_slide() # Waits user to press continue to go to the next slide
self.start_loop() # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop() # This will loop until user inputs a key
self.play(dot.animate.move_to(ORIGIN))
self.pause() # Waits user to press continue to go to the next slide
self.next_slide() # Waits user to press continue to go to the next slide
class MultipleAnimationsInLastSlide(Slide):
"""This is used to check against solution for issue #161."""
def construct(self):
circle = Circle(color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
self.next_slide()
class TestFileTooLong(Slide):
"""This is used to check against solution for issue #123."""
def construct(self):
import random
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
for _ in range(30):
direction = (random.random() - 0.5) * LEFT + (random.random() - 0.5) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
self.next_slide()
class ConvertExample(Slide):
@ -39,7 +76,6 @@ class ConvertExample(Slide):
self.wait(0.1)
def construct(self):
title = VGroup(
Text("From Manim animations", t2c={"From": BLUE}),
Text("to slides presentation", t2c={"to": BLUE}),
@ -60,7 +96,7 @@ class ConvertExample(Slide):
self.play(FadeIn(title))
self.pause()
self.next_slide()
code = Code(
code="""from manim import *
@ -129,10 +165,10 @@ class Example(Slide):
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
self.pause()
self.next_slide()
square = Square()
self.play(Transform(dot, square))
self.pause()
self.next_slide()
self.play(Rotate(square, angle=PI/2))
""",
language="python",
@ -151,7 +187,7 @@ class Example(Slide):
self.end_loop()
square = Square()
self.play(Transform(dot, square))
self.pause()
self.next_slide()
self.play(Rotate(square, angle=PI/2))
""",
language="python",
@ -178,38 +214,38 @@ class Example(Slide):
self.play(FadeIn(code))
self.tinywait()
self.pause()
self.next_slide()
self.play(FadeIn(step, shift=RIGHT))
self.play(Transform(code, code_step_1))
self.tinywait()
self.pause()
self.next_slide()
self.play(Transform(step, step_2))
self.play(Transform(code, code_step_2))
self.tinywait()
self.pause()
self.next_slide()
self.play(Transform(step, step_3))
self.play(Transform(code, code_step_3))
self.tinywait()
self.pause()
self.next_slide()
self.play(Transform(step, step_4))
self.play(Transform(code, code_step_4))
self.tinywait()
self.pause()
self.next_slide()
self.play(Transform(step, step_5))
self.play(Transform(code, code_step_5))
self.tinywait()
self.pause()
self.next_slide()
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()
self.next_slide()
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
@ -229,10 +265,10 @@ class Example(Slide):
self.remove(dot)
self.add(square)
self.tinywait()
self.pause()
self.next_slide()
self.play(Rotate(square, angle=PI / 4))
self.tinywait()
self.pause()
self.next_slide()
learn_more_text = (
VGroup(
@ -250,7 +286,6 @@ class Example(Slide):
# For ThreeDExample, things are different
if not MANIMGL:
# [manim-3d]
class ThreeDExample(ThreeDSlide):
def construct(self):
@ -265,7 +300,7 @@ if not MANIMGL:
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.pause()
self.next_slide()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
@ -275,16 +310,15 @@ if not MANIMGL:
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.next_slide()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.next_slide()
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))
# [manim-3d]
@ -315,7 +349,7 @@ else:
updater = lambda m, dt: m.increment_theta((75 * DEGREES / 4) * dt)
frame.add_updater(updater)
self.pause()
self.next_slide()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
@ -324,16 +358,15 @@ else:
frame.remove_updater(updater)
self.play(frame.animate.set_theta(30 * DEGREES))
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.next_slide()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.next_slide()
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))
# [manimgl-3d]

View File

@ -1,3 +1,48 @@
# flake8: noqa: F401
import sys
from types import ModuleType
from typing import Any, List
from .__version__ import __version__
from .slide import Slide, ThreeDSlide
class module(ModuleType):
def __getattr__(self, name: str) -> Any:
if name == "Slide" or name == "ThreeDSlide":
module = __import__(
"manim_slides.slide", None, None, ["Slide", "ThreeDSlide"]
)
return getattr(module, name)
return ModuleType.__getattribute__(self, name)
def __dir__(self) -> List[str]:
result = list(new_module.__all__)
result.extend(
(
"__file__",
"__doc__",
"__all__",
"__docformat__",
"__name__",
"__path__",
"__package__",
"__version__",
)
)
return result
old_module = sys.modules["manim_slides"]
new_module = sys.modules["manim_slides"] = module("manim_slides")
new_module.__dict__.update(
{
"__file__": __file__,
"__package__": "manim_slides",
"__path__": __path__,
"__doc__": __doc__,
"__version__": __version__,
"__all__": ("__version__", "Slides", "ThreeDSlide"),
}
)

View File

@ -4,9 +4,9 @@ import click
import requests
from click_default_group import DefaultGroup
from . import __version__
from .__version__ import __version__
from .convert import convert
from .manim import logger
from .logger import make_logger
from .present import list_scenes, present
from .wizard import init, wizard
@ -27,6 +27,7 @@ def cli(notify_outdated_version: bool) -> None:
If no command is specified, defaults to `present`.
"""
logger = make_logger()
# Code below is mostly a copy from:
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
if notify_outdated_version:

View File

@ -1 +1 @@
__version__ = "4.8.3"
__version__ = "4.13.2"

View File

@ -1,10 +1,11 @@
from pathlib import Path
from typing import Any, Callable
import click
from click import Context, Parameter
from .defaults import CONFIG_PATH, FOLDER_PATH
from .manim import logger
from .logger import logger
F = Callable[..., Any]
Wrapper = Callable[[F], F]
@ -18,7 +19,7 @@ def config_path_option(function: F) -> F:
"config_path",
metavar="FILE",
default=CONFIG_PATH,
type=click.Path(dir_okay=False),
type=click.Path(dir_okay=False, path_type=Path),
help="Set path to configuration file.",
show_default=True,
)
@ -44,7 +45,6 @@ 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
@ -54,10 +54,10 @@ def verbosity_option(function: F) -> F:
"-v",
"--verbosity",
type=click.Choice(
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
["PERF", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
case_sensitive=False,
),
help="Verbosity of CLI output",
help="Verbosity of CLI output. PERF will log performances (timing) information.",
default=None,
expose_value=False,
envvar="MANIM_SLIDES_VERBOSITY",
@ -74,7 +74,7 @@ def folder_path_option(function: F) -> F:
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
type=click.Path(exists=True, file_okay=False, path_type=Path),
help="Set slides folder.",
show_default=True,
)

View File

@ -1,27 +1,38 @@
import hashlib
import os
import shutil
import subprocess
import tempfile
from enum import Enum
from typing import Callable, Dict, List, Optional, Set, Union
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel, root_validator, validator
from pydantic import BaseModel, FilePath, PositiveInt, root_validator, validator
from pydantic.color import Color
from PySide6.QtCore import Qt
from .manim import FFMPEG_BIN, logger
from .defaults import FFMPEG_BIN
from .logger import logger
def merge_basenames(files: List[str]) -> str:
def merge_basenames(files: List[FilePath]) -> Path:
"""
Merge multiple filenames by concatenating basenames.
"""
logger.info(f"Generating a new filename for animations: {files}")
dirname = os.path.dirname(files[0])
_, ext = os.path.splitext(files[0])
dirname: Path = files[0].parent
ext = files[0].suffix
basename = "_".join(os.path.splitext(os.path.basename(file))[0] for file in files)
basenames = (file.stem for file in files)
return os.path.join(dirname, basename + ext)
basenames_str = ",".join(f"{len(b)}:{b}" for b in basenames)
# We use hashes to prevent too-long filenames, see issue #123:
# https://github.com/jeertmans/manim-slides/issues/123
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
return dirname.joinpath(basename + ext)
class Key(BaseModel): # type: ignore
@ -111,7 +122,6 @@ class SlideConfig(BaseModel): # type: ignore
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(...)`."
@ -139,26 +149,16 @@ class SlideConfig(BaseModel): # type: ignore
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
files: List[FilePath]
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black"
@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")
cls, values: Dict[str, Union[List[SlideConfig], List[FilePath]]]
) -> Dict[str, Union[List[SlideConfig], List[FilePath]]]:
files: List[FilePath] = values.get("files") # type: ignore
slides: List[SlideConfig] = values.get("slides") # type: ignore
if files is None or slides is None:
return values
@ -166,33 +166,33 @@ class PresentationConfig(BaseModel): # type: ignore
n_files = len(files)
for slide in slides:
if slide.end_animation > n_files: # type: ignore
if slide.end_animation > n_files:
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":
def copy_to(self, dest: Path, use_cached: bool = True) -> "PresentationConfig":
"""
Moves (or copy) the files to a given directory.
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)
dest_path = dest / self.files[i].name
self.files[i] = dest_path
if use_cached and dest_path.exists():
logger.debug(f"Skipping copy of {file}, using cached copy")
continue
logger.debug(f"Copying {file} to {dest_path}")
shutil.copy(file, dest_path)
return self
def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
def concat_animations(
self, dest: Optional[Path] = None, use_cached: bool = True
) -> "PresentationConfig":
"""
Concatenate animations such that each slide contains one animation.
"""
@ -202,14 +202,22 @@ class PresentationConfig(BaseModel): # type: ignore
for i, slide_config in enumerate(self.slides):
files = self.files[slide_config.slides_slice]
slide_config.start_animation = i
slide_config.end_animation = i + 1
if len(files) > 1:
dest_path = merge_basenames(files)
dest_paths.append(dest_path)
if use_cached and dest_path.exists():
logger.debug(f"Concatenated animations already exist for slide {i}")
continue
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
f.writelines(f"file '{os.path.abspath(path)}'\n" for path in files)
f.close()
command = [
command: List[str] = [
FFMPEG_BIN,
"-f",
"concat",
@ -219,7 +227,7 @@ class PresentationConfig(BaseModel): # type: ignore
f.name,
"-c",
"copy",
dest_path,
str(dest_path),
"-y",
]
logger.debug(" ".join(command))
@ -234,18 +242,18 @@ class PresentationConfig(BaseModel): # type: ignore
if error:
logger.debug(error.decode())
dest_paths.append(dest_path)
if not dest_path.exists():
raise ValueError(
"could not properly concatenate animations, use `-v INFO` for more details"
)
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.copy_to(dest)
return self

View File

@ -1,22 +1,42 @@
import os
import platform
import subprocess
import sys
import tempfile
import webbrowser
from enum import Enum
from importlib import resources
from pathlib import Path
from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union
import click
import pkg_resources
import cv2
import pptx
from click import Context, Parameter
from pydantic import BaseModel, PositiveInt, ValidationError
from lxml import etree
from pydantic import BaseModel, FilePath, PositiveInt, ValidationError
from tqdm import tqdm
from . import data
from .commons import folder_path_option, verbosity_option
from .config import PresentationConfig
from .logger import logger
from .present import get_scenes_presentation_config
def open_with_default(file: Path) -> None:
system = platform.system()
if system == "Darwin":
subprocess.call(("open", str(file)))
elif system == "Windows":
os.startfile(str(file)) # type: ignore[attr-defined]
else:
subprocess.call(("xdg-open", str(file)))
def validate_config_option(
ctx: Context, param: Parameter, value: Any
) -> Dict[str, str]:
config = {}
for c_option in value:
@ -34,9 +54,9 @@ def validate_config_option(
class Converter(BaseModel): # type: ignore
presentation_configs: List[PresentationConfig] = []
assets_dir: str = "{basename}_assets"
template: Optional[str] = None
template: Optional[Path] = None
def convert_to(self, dest: str) -> None:
def convert_to(self, dest: Path) -> None:
"""Converts self, i.e., a list of presentations, into a given format."""
raise NotImplementedError
@ -46,7 +66,7 @@ class Converter(BaseModel): # type: ignore
An empty string is returned if no template is used."""
return ""
def open(self, file: str) -> bool:
def open(self, file: Path) -> Any:
"""Opens a file, generated with converter, using appropriate application."""
raise NotImplementedError
@ -55,6 +75,7 @@ class Converter(BaseModel): # type: ignore
"""Returns the appropriate converter from a string name."""
return {
"html": RevealJS,
"pptx": PowerPoint,
}[s]
@ -171,6 +192,13 @@ class TransitionSpeed(Str, Enum): # type: ignore
slow = "slow"
class BackgroundSize(Str, Enum): # type: ignore
# From: https://developer.mozilla.org/en-US/docs/Web/CSS/background-size
# TODO: support more background size
contain = "contain"
cover = "cover"
BackgroundTransition = Transition
@ -259,6 +287,7 @@ class RevealJS(Converter):
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
background_transition: BackgroundTransition = BackgroundTransition.none
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
pdf_separate_fragments: JsBool = JsBool.true
@ -278,12 +307,14 @@ class RevealJS(Converter):
use_enum_values = True
extra = "forbid"
def get_sections_iter(self) -> Generator[str, None, None]:
def get_sections_iter(self, assets_dir: Path) -> 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))
file = assets_dir / file.name
logger.debug(f"Writing video section with file {file}")
# TODO: document this
# Videos are muted because, otherwise, the first slide never plays correctly.
@ -292,40 +323,43 @@ class RevealJS(Converter):
# Read more about this:
# https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_and_autoplay_blocking
if slide_config.is_loop():
yield f'<section data-background-video="{file}" data-background-video-muted data-background-video-loop></section>'
yield f'<section data-background-size={self.background_size.value} data-background-color="{presentation_config.background_color}" 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>'
yield f'<section data-background-size={self.background_size.value} data-background-color="{presentation_config.background_color}" 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()
if isinstance(self.template, Path):
return self.template.read_text()
def open(self, file: str) -> bool:
return webbrowser.open(file)
if sys.version_info < (3, 9):
return resources.read_text(data, "revealjs_template.html")
def convert_to(self, dest: str) -> None:
return resources.files(data).joinpath("revealjs_template.html").read_text()
def open(self, file: Path) -> bool:
return webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> 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))
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
self.assets_dir = self.assets_dir.format(
dirname=dirname, basename=basename, ext=ext
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = os.path.join(dirname, self.assets_dir)
full_assets_dir = dirname / assets_dir
logger.debug(f"Assets will be saved to: {full_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)
presentation_config.concat_animations().copy_to(full_assets_dir)
with open(dest, "w") as f:
sections = "".join(self.get_sections_iter())
sections = "".join(self.get_sections_iter(assets_dir))
revealjs_template = self.load_template()
content = revealjs_template.format(sections=sections, **self.dict())
@ -333,11 +367,96 @@ class RevealJS(Converter):
f.write(content)
class PowerPoint(Converter):
left: PositiveInt = 0
top: PositiveInt = 0
width: PositiveInt = 1280
height: PositiveInt = 720
auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None
class Config:
use_enum_values = True
extra = "forbid"
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
"""Converts this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
prs.slide_width = self.width * 9525
prs.slide_height = self.height * 9525
layout = prs.slide_layouts[6] # Should be blank
# From GitHub issue comment:
# - https://github.com/scanny/python-pptx/issues/427#issuecomment-856724440
def auto_play_media(
media: pptx.shapes.picture.Movie, loop: bool = False
) -> None:
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
el_cnt = xpath(
media.element.getparent().getparent().getparent(),
'.//p:timing//p:video//p:spTgt[@spid="%s"]' % el_id,
)[0]
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
cond.set("delay", "0")
if loop:
ctn = xpath(el_cnt.getparent().getparent(), ".//p:cTn")[0]
ctn.set("repeatCount", "indefinite")
def xpath(el: etree.Element, query: str) -> etree.XPath:
nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
return etree.ElementBase.xpath(el, query, namespaces=nsmap)
def save_first_image_from_video_file(file: Path) -> Optional[str]:
cap = cv2.VideoCapture(str(file))
ret, frame = cap.read()
if ret:
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
cv2.imwrite(f.name, frame)
return f.name
else:
logger.warn("Failed to read first image from video file")
return None
for i, presentation_config in enumerate(self.presentation_configs):
presentation_config.concat_animations()
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
file = presentation_config.files[slide_config.start_animation]
if self.poster_frame_image is None:
poster_frame_image = save_first_image_from_video_file(file)
else:
poster_frame_image = str(self.poster_frame_image)
slide = prs.slides.add_slide(layout)
movie = slide.shapes.add_movie(
str(file),
self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type="video/mp4",
)
if self.auto_play_media:
auto_play_media(movie, loop=slide_config.is_loop())
prs.save(dest)
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
@ -349,7 +468,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
ctx.exit()
return click.option(
return click.option( # type: ignore
"--show-config",
is_flag=True,
help="Show supported options for given format and exit.",
@ -364,7 +483,6 @@ 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
@ -378,7 +496,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
ctx.exit()
return click.option(
return click.option( # type: ignore
"--show-template",
is_flag=True,
help="Show the template (currently) used for a given conversion format and exit.",
@ -392,10 +510,10 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
@click.command()
@click.argument("scenes", nargs=-1)
@folder_path_option
@click.argument("dest")
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
@click.option(
"--to",
type=click.Choice(["html"], case_sensitive=False),
type=click.Choice(["html", "pptx"], case_sensitive=False),
default="html",
show_default=True,
help="Set the conversion format to use.",
@ -419,7 +537,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"--use-template",
"template",
metavar="FILE",
type=click.Path(exists=True, dir_okay=False),
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="Use the template given by FILE instead of default one. To echo the default template, use `--show-template`.",
)
@show_template_option
@ -427,13 +545,13 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
@verbosity_option
def convert(
scenes: List[str],
folder: str,
dest: str,
folder: Path,
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: Dict[str, str],
template: Optional[str],
template: Optional[Path],
) -> None:
"""
Convert SCENE(s) into a given format and writes the result in DEST.
@ -454,7 +572,6 @@ def convert(
converter.open(dest)
except ValidationError as e:
errors = e.errors()
msg = [

View File

View File

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

47
manim_slides/logger.py Normal file
View File

@ -0,0 +1,47 @@
"""
Logger utils, mostly copied from Manim Community:
https://github.com/ManimCommunity/manim/blob/d5b65b844b8ce8ff5151a2f56f9dc98cebbc1db4/manim/_config/logger_utils.py#L29-L101
"""
import logging
from rich.console import Console
from rich.logging import RichHandler
from rich.theme import Theme
__all__ = ["logger", "make_logger"]
HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
"Played",
"animations",
"scene",
"Reading",
"Writing",
"script",
"arguments",
"Invalid",
"Aborting",
"module",
"File",
"Rendering",
"Rendered",
]
def make_logger() -> logging.Logger:
"""
Make a logger similar to the one used by Manim.
"""
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
rich_handler = RichHandler(
show_time=True,
console=Console(theme=Theme({"logging.level.perf": "magenta"})),
)
logging.addLevelName(5, "PERF")
logger = logging.getLogger("manim-slides")
logger.addHandler(rich_handler)
return logger
logger = logging.getLogger("manim-slides")

View File

@ -3,12 +3,15 @@ import platform
import sys
import time
from enum import Enum, IntEnum, auto, unique
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import click
import cv2
import numpy as np
from click import Context, Parameter
from pydantic import ValidationError
from pydantic.color import Color
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
@ -17,7 +20,7 @@ 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 .logger import logger
from .resources import * # noqa: F401, F403
os.environ.pop(
@ -69,10 +72,9 @@ 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.config = config
self.current_slide_index: int = 0
self.__current_slide_index: int = 0
self.current_animation: int = self.current_slide.start_animation
self.current_file: str = ""
@ -86,6 +88,71 @@ class Presentation:
self.reset()
def __len__(self) -> int:
return len(self.slides)
@property
def slides(self) -> List[SlideConfig]:
"""Returns the list of slides."""
return self.config.slides
@property
def files(self) -> List[Path]:
"""Returns the list of animation files."""
return self.config.files
@property
def resolution(self) -> Tuple[int, int]:
"""Returns the resolution."""
return self.config.resolution
@property
def background_color(self) -> Color:
"""Returns the background color."""
return self.config.background_color
@property
def current_slide_index(self) -> int:
return self.__current_slide_index
@current_slide_index.setter
def current_slide_index(self, value: Optional[int]) -> None:
if value is not None:
if -len(self) <= value < len(self):
self.__current_slide_index = value
self.current_animation = self.current_slide.start_animation
logger.debug(f"Set current slide index to {value}")
else:
logger.error(
f"Could not load slide number {value}, playing first slide instead."
)
def set_current_animation_and_update_slide_number(
self, value: Optional[int]
) -> None:
if value is not None:
n_files = len(self.files)
if -n_files <= value < n_files:
if value < 0:
value += n_files
for i, slide in enumerate(self.slides):
if value < slide.end_animation:
self.current_slide_index = i
self.current_animation = value
logger.debug(f"Playing animation {value}, at slide index {i}")
return
assert (
False
), f"An error occurred when setting the current animation to {value}, please create an issue on GitHub!"
else:
logger.error(
f"Could not load animation number {value}, playing first animation instead."
)
@property
def current_slide(self) -> SlideConfig:
"""Returns currently playing slide."""
@ -114,12 +181,11 @@ class Presentation:
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]
file: str = str(self.files[animation])
if self.reverse:
file = "{}_reversed{}".format(*os.path.splitext(file))
@ -127,7 +193,7 @@ class Presentation:
self.current_file = file
self.cap = cv2.VideoCapture(file)
self.cap = cv2.VideoCapture(str(file))
self.loaded_animation_cap = animation
@property
@ -138,7 +204,9 @@ class Presentation:
def rewind_current_slide(self) -> None:
"""Rewinds current slide to first frame."""
logger.debug("Rewinding curring slide")
logger.debug("Rewinding current slide")
self.current_slide.terminated = False
if self.reverse:
self.current_animation = self.current_slide.end_animation - 1
else:
@ -176,9 +244,10 @@ class Presentation:
def load_previous_slide(self) -> None:
"""Loads previous slide."""
logger.debug("Loading previous slide")
logger.debug(f"Loading previous slide, current is {self.current_slide_index}")
self.cancel_reverse()
self.current_slide_index = max(0, self.current_slide_index - 1)
logger.debug(f"Loading slide index {self.current_slide_index}")
self.rewind_current_slide()
@property
@ -189,7 +258,8 @@ class Presentation:
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
# TODO: understand why we sometimes get 0 fps
return max(fps, 1) # type: ignore
def reset(self) -> None:
"""Rests current presentation."""
@ -217,7 +287,7 @@ class Presentation:
return self.current_animation + 1
@property
def is_last_animation(self) -> int:
def is_last_animation(self) -> bool:
"""Returns True if current animation is the last one of current slide."""
if self.reverse:
return self.current_animation == self.current_slide.start_animation
@ -280,16 +350,20 @@ class Display(QThread): # type: ignore
change_video_signal = Signal(np.ndarray)
change_info_signal = Signal(dict)
change_presentation_sigal = Signal()
finished = Signal()
def __init__(
self,
presentations: List[PresentationConfig],
presentations: List[Presentation],
config: Config = DEFAULT_CONFIG,
start_paused: bool = False,
skip_all: bool = False,
record_to: Optional[str] = None,
exit_after_last_slide: bool = False,
start_at_scene_number: Optional[int] = None,
start_at_slide_number: Optional[int] = None,
start_at_animation_number: Optional[int] = None,
) -> None:
super().__init__()
self.presentations = presentations
@ -301,17 +375,53 @@ class Display(QThread): # type: ignore
self.state = State.PLAYING
self.lastframe: Optional[np.ndarray] = None
self.current_presentation_index = 0
self.__current_presentation_index = 0
self.current_presentation_index = start_at_scene_number # type: ignore
self.current_presentation.current_slide_index = start_at_slide_number # type: ignore
self.current_presentation.set_current_animation_and_update_slide_number(
start_at_animation_number
)
self.run_flag = True
self.key = -1
self.exit_after_last_slide = exit_after_last_slide
def __len__(self) -> int:
return len(self.presentations)
@property
def current_presentation_index(self) -> int:
return self.__current_presentation_index
@current_presentation_index.setter
def current_presentation_index(self, value: Optional[int]) -> None:
if value is not None:
if -len(self) <= value < len(self):
self.__current_presentation_index = value
self.current_presentation.release_cap()
self.change_presentation_sigal.emit()
else:
logger.error(
f"Could not load scene number {value}, playing first scene instead."
)
@property
def current_presentation(self) -> Presentation:
"""Returns the current presentation."""
return self.presentations[self.current_presentation_index]
@property
def current_resolution(self) -> Tuple[int, int]:
"""Returns the resolution of the current presentation."""
return self.current_presentation.resolution
@property
def current_background_color(self) -> Color:
"""Returns the background color of the current presentation."""
return self.current_presentation.background_color
def run(self) -> None:
"""Runs a series of presentations until end or exit."""
while self.run_flag:
@ -338,6 +448,21 @@ class Display(QThread): # type: ignore
lag = now() - last_time
sleep_time = 1 / self.current_presentation.fps
logger.log(
5,
f"Took {lag:.3f} seconds to process the current frame, that must play at a rate of one every {sleep_time:.3f} seconds.",
)
if sleep_time - lag < 0:
logger.warn(
"The FPS rate could not be matched. "
"This is normal when manually transitioning between slides.\n"
"If you feel that the FPS are too low, "
"consider checking this issue:\n"
"https://github.com/jeertmans/manim-slides/issues/179."
)
sleep_time = max(sleep_time - lag, 0)
time.sleep(sleep_time)
last_time = now()
@ -346,7 +471,7 @@ class Display(QThread): # type: ignore
if self.record_to is not None:
self.record_movie()
logger.debug("Closing video thread gracully and exiting")
logger.debug("Closing video thread gracefully and exiting")
self.finished.emit()
def record_movie(self) -> None:
@ -520,7 +645,6 @@ class App(QWidget): # type: ignore
*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,
@ -532,7 +656,12 @@ class App(QWidget): # type: ignore
self.setWindowTitle(WINDOW_NAME)
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.display_width, self.display_height = resolution
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
self.display_width, self.display_height = self.thread.current_resolution
self.aspect_ratio = aspect_ratio
self.resize_mode = resize_mode
self.hide_mouse = hide_mouse
@ -546,15 +675,14 @@ class App(QWidget): # type: ignore
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.label.setStyleSheet(
f"background-color: {self.thread.current_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()
@ -568,6 +696,7 @@ class App(QWidget): # type: ignore
# connect signals
self.thread.change_video_signal.connect(self.update_image)
self.thread.change_info_signal.connect(self.info.update_info)
self.thread.change_presentation_sigal.connect(self.update_canvas)
self.thread.finished.connect(self.closeAll)
self.send_key_signal.connect(self.thread.set_key)
@ -575,7 +704,6 @@ class App(QWidget): # type: ignore
self.thread.start()
def keyPressEvent(self, event: QKeyEvent) -> None:
key = event.key()
if self.config.HIDE_MOUSE.match(key):
if self.hide_mouse:
@ -622,47 +750,58 @@ class App(QWidget): # type: ignore
self.label.setPixmap(QPixmap.fromImage(qt_img))
@Slot()
def update_canvas(self) -> None:
"""Update the canvas when a presentation has changed."""
logger.debug("Updating canvas")
self.display_width, self.display_height = self.thread.current_resolution
if not self.isFullScreen():
self.resize(self.display_width, self.display_height)
self.label.setStyleSheet(
f"background-color: {self.thread.current_background_color}"
)
@click.command()
@click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
type=click.Path(exists=True, file_okay=False, path_type=Path),
help="Set slides folder.",
show_default=True,
)
@click.help_option("-h", "--help")
@verbosity_option
def list_scenes(folder: str) -> None:
def list_scenes(folder: Path) -> 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]:
def _list_scenes(folder: Path) -> 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
for filepath in folder.glob("*.json"):
try:
_ = PresentationConfig.parse_file(filepath)
scenes.append(filepath.stem)
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]:
def prompt_for_scenes(folder: Path) -> List[str]:
"""Prompts the user to select scenes within a given folder."""
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
@ -691,13 +830,13 @@ def prompt_for_scenes(folder: str) -> List[str]:
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
return scenes
return scenes # type: ignore
except ValueError as e:
raise click.UsageError(str(e))
def get_scenes_presentation_config(
scenes: List[str], folder: str
scenes: List[str], folder: Path
) -> List[PresentationConfig]:
"""Returns a list of presentation configurations based on the user input."""
@ -706,8 +845,8 @@ def get_scenes_presentation_config(
presentation_configs = []
for scene in scenes:
config_file = os.path.join(folder, f"{scene}.json")
if not os.path.exists(config_file):
config_file = folder / f"{scene}.json"
if not config_file.exists():
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"
)
@ -719,6 +858,37 @@ def get_scenes_presentation_config(
return presentation_configs
def start_at_callback(
ctx: Context, param: Parameter, values: str
) -> Tuple[Optional[int], ...]:
if values == "(None, None, None)":
return (None, None, None)
def str_to_int_or_none(value: str) -> Optional[int]:
if value.lower().strip() == "":
return None
else:
try:
return int(value)
except ValueError:
raise click.BadParameter(
f"start index can only be an integer or an empty string, not `{value}`",
ctx=ctx,
param=param,
)
values_tuple = values.split(",")
n_values = len(values_tuple)
if n_values == 3:
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 3 arguments are expected but you gave {n_values}, please use commas to separate them",
ctx=ctx,
param=param,
)
@click.command()
@click.argument("scenes", nargs=-1)
@config_path_option
@ -726,7 +896,7 @@ def get_scenes_presentation_config(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
type=click.Path(exists=True, file_okay=False, path_type=Path),
help="Set slides folder.",
show_default=True,
)
@ -736,23 +906,22 @@ def get_scenes_presentation_config(
"-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.",
help="Skip all slides, useful the test if slides are working. Automatically sets `--exit-after-last-slide` to True.",
)
@click.option(
"-r",
"--resolution",
metavar="<WIDTH HEIGHT>",
type=(int, int),
default=(1920, 1080),
default=None,
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),
type=click.Path(dir_okay=False, path_type=Path),
default=None,
help="If set, the presentation will be recorded into a AVI video file with given name.",
)
@ -786,26 +955,67 @@ def get_scenes_presentation_config(
"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)".',
default=None,
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)". If not set, it defaults to the background color configured in the Manim scene.',
show_default=True,
)
@click.option(
"--sa",
"--start-at",
"start_at",
metavar="<SCENE,SLIDE,ANIMATION>",
type=str,
callback=start_at_callback,
default=(None, None, None),
help="Start presenting at (x, y, z), equivalent to --sacn x --sasn y --saan z, and overrides values if not None.",
)
@click.option(
"--sacn",
"--start-at-scene-number",
"start_at_scene_number",
metavar="INDEX",
type=int,
default=None,
help="Start presenting at a given scene number (0 is first, -1 is last).",
)
@click.option(
"--sasn",
"--start-at-slide-number",
"start_at_slide_number",
metavar="INDEX",
type=int,
default=None,
help="Start presenting at a given slide number (0 is first, -1 is last).",
)
@click.option(
"--saan",
"--start-at-animation-number",
"start_at_animation_number",
metavar="INDEX",
type=int,
default=0,
help="Start presenting at a given animation number (0 is first, -1 is last). This conflicts with slide number since animation number is absolute to the presentation.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
scenes: List[str],
config_path: str,
folder: str,
config_path: Path,
folder: Path,
start_paused: bool,
fullscreen: bool,
skip_all: bool,
resolution: Tuple[int, int],
record_to: Optional[str],
resolution: Optional[Tuple[int, int]],
record_to: Optional[Path],
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
resize_mode: str,
background_color: str,
background_color: Optional[str],
start_at: Tuple[Optional[int], Optional[int], Optional[int]],
start_at_scene_number: Optional[int],
start_at_slide_number: Optional[int],
start_at_animation_number: Optional[int],
) -> None:
"""
Present SCENE(s), one at a time, in order.
@ -820,12 +1030,22 @@ def present(
if skip_all:
exit_after_last_slide = True
presentation_configs = get_scenes_presentation_config(scenes, folder)
if resolution is not None:
for presentation_config in presentation_configs:
presentation_config.resolution = resolution
if background_color is not None:
for presentation_config in presentation_configs:
presentation_config.background_color = background_color
presentations = [
Presentation(presentation_config)
for presentation_config in get_scenes_presentation_config(scenes, folder)
for presentation_config in presentation_configs
]
if os.path.exists(config_path):
if config_path.exists():
try:
config = Config.parse_file(config_path)
except ValidationError as e:
@ -835,12 +1055,21 @@ def present(
config = Config()
if record_to is not None:
_, ext = os.path.splitext(record_to)
ext = record_to.suffix
if ext.lower() != ".avi":
raise click.UsageError(
"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
)
if start_at[0]:
start_at_scene_number = start_at[0]
if start_at[1]:
start_at_slide_number = start_at[1]
if start_at[2]:
start_at_animation_number = start_at[2]
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides")
a = App(
@ -849,13 +1078,14 @@ def present(
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,
start_at_scene_number=start_at_scene_number,
start_at_slide_number=start_at_slide_number,
start_at_animation_number=start_at_animation_number,
)
a.show()
sys.exit(app.exec_())

View File

@ -2,7 +2,8 @@ import os
import platform
import shutil
import subprocess
from typing import Any, List, Optional
from typing import Any, List, Optional, Tuple
from warnings import warn
from tqdm import tqdm
@ -46,15 +47,31 @@ class Slide(Scene): # type:ignore
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
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]:
def __background_color(self) -> str:
"""Returns the scene's background color."""
if MANIMGL:
return self.camera_config["background_color"].hex # type: ignore
else:
return config["background_color"].hex # type: ignore
@property
def __resolution(self) -> Tuple[int, int]:
"""Returns the scene's resolution used during rendering."""
if MANIMGL:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
else:
return config["pixel_width"], config["pixel_height"]
@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
@ -63,100 +80,192 @@ class Slide(Scene): # type:ignore
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
return get_sorted_integer_files( # type: ignore
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
return self.renderer.file_writer.partial_movie_files # type: ignore
@property
def show_progress_bar(self) -> bool:
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)
return getattr(self, "show_progress_bar", True)
else:
return config["progress_bar"] != "none"
return config["progress_bar"] != "none" # type: ignore
@property
def leave_progress_bar(self) -> bool:
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)
return getattr(self, "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
return config["progress_bar"] == "leave" # type: ignore
@property
def __start_at_animation_number(self) -> Optional[int]:
if MANIMGL:
return getattr(self, "start_at_animation_number", None)
else:
return config["from_animation_number"] # type: ignore
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overloads `self.play` and increment animation count."""
super().play(*args, **kwargs)
self.current_animation += 1
self.__current_animation += 1
def pause(self) -> None:
"""Creates a new slide with previous animations."""
self.slides.append(
def next_slide(self) -> None:
"""
Creates a new slide with previous animations.
This usually means that the user will need to press some key before the
next slide is played. By default, this is the right arrow key.
.. note::
Calls to :func:`next_slide` at the very beginning or at the end are
not needed, since they are automatically added.
.. warning::
This is not allowed to call :func:`next_slide` inside a loop.
Examples
--------
The following contains 3 slides:
#. the first with nothing on it;
#. the second with "Hello World!" fading in;
#. and the last with the text fading out;
.. code-block:: python
from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
text = Text("Hello World!")
self.next_slide()
self.play(FadeIn(text))
self.next_slide()
self.play(FadeOut(text))
"""
assert (
self.__loop_start_animation is None
), "You cannot call `self.next_slide()` inside a loop"
self.__slides.append(
SlideConfig(
type=SlideType.slide,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_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
self.__current_slide += 1
self.__pause_start_animation = self.__current_animation
def add_last_slide(self) -> None:
def pause(self) -> None:
"""
Creates a new slide with previous animations.
.. deprecated:: 4.10.0
Use :func:`next_slide` instead.
"""
warn(
"`self.pause()` is deprecated. Use `self.next_slide()` instead.",
DeprecationWarning,
stacklevel=2,
)
Slide.next_slide(self)
def __add_last_slide(self) -> None:
"""Adds a 'last' slide to the end of slides."""
if self.current_animation == self.slides[-1].end_animation:
self.slides[-1].type = SlideType.last
if (
len(self.__slides) > 0
and self.__current_animation == self.__slides[-1].end_animation
):
self.__slides[-1].type = SlideType.last
return
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.last,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
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
"""
Starts a loop. End it with :func:`end_loop`.
A loop will automatically replay the slide, i.e., everything between
:func:`start_loop` and :func:`end_loop`, upon reaching end.
Examples
--------
The following contains one slide that will loop endlessly.
.. code-block:: python
from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot(color=BLUE)
self.start_loop()
self.play(Indicate(dot))
self.end_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."""
"""Ends an existing loop. See :func:`start_loop` for more details."""
assert (
self.loop_start_animation is not None
self.__loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.loop,
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
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
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:
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()
self.__add_last_slide()
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
if not os.path.exists(self.__output_folder):
os.mkdir(self.__output_folder)
files_folder = os.path.join(self.output_folder, "files")
files_folder = os.path.join(self.__output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
scene_name = type(self).__name__
scene_name = str(self)
scene_files_folder = os.path.join(files_folder, scene_name)
old_animation_files = set()
@ -171,12 +280,18 @@ class Slide(Scene): # type:ignore
files = []
for src_file in tqdm(
self.partial_movie_files,
self.__partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=self.leave_progress_bar,
leave=self.__leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self.show_progress_bar,
disable=not self.__show_progress_bar,
):
if src_file is None and not MANIMGL:
# This happens if rendering with -na,b (manim only)
# where animations not in [a,b] will be skipped
# but animations before a will have a None src_file
continue
filename = os.path.basename(src_file)
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
@ -196,14 +311,30 @@ class Slide(Scene): # type:ignore
files.append(dst_file)
if offset := self.__start_at_animation_number:
self.__slides = [
slide for slide in self.__slides if slide.end_animation > offset
]
for slide in self.__slides:
slide.start_animation -= offset
slide.end_animation -= offset
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,))
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))
f.write(
PresentationConfig(
slides=self.__slides,
files=files,
resolution=self.__resolution,
background_color=self.__background_color,
).json(indent=2)
)
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
@ -212,11 +343,11 @@ class Slide(Scene): # type:ignore
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
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
# We need to disable the caching limit since we rely on intermediate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
@ -224,7 +355,7 @@ class Slide(Scene): # type:ignore
config["max_files_cached"] = max_files_cached
self.save_slides()
self.__save_slides()
class ThreeDSlide(Slide, ThreeDScene): # type: ignore

View File

@ -21,7 +21,7 @@ from PySide6.QtWidgets import (
from .commons import config_options, verbosity_option
from .config import Config, Key
from .defaults import CONFIG_PATH
from .manim import logger
from .logger import logger
from .resources import * # noqa: F401, F403
WINDOW_NAME: str = "Configuration Wizard"
@ -51,7 +51,6 @@ class KeyInput(QDialog): # type: ignore
class Wizard(QWidget): # type: ignore
def __init__(self, config: Config):
super().__init__()
self.setWindowTitle(WINDOW_NAME)

BIN
paper/docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

53
paper/paper.bib Normal file
View File

@ -0,0 +1,53 @@
@online{manim-announcement,
author = {Grant Sanderson},
title = {{Q}\&{A} with {G}rant {S}anderson (3blue1brown)},
year = {2018},
organization = {YouTube},
url = {https://www.youtube.com/watch?v=Qe6o9j4IjTo\&ab_channel=3Blue1Brown}
}
@misc{revealjs,
author = {Hakim El Hattab},
title = {The HTML Presentation Framework},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/hakimel/reveal.js}
}
@misc{manim-presentation,
author = {Federico Galatolo},
title = {Tool for live presentations using manim},
year = {2021},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/galatolofederico/manim-presentation}
}
@misc{manimgl,
author = {Grant Sanderson},
title = {Animation engine for explanatory math videos},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/3b1b/manim}
}
@misc{manim-editor,
author = {Christopher Besch},
title = {Web Presenter for Mathematical Animations using Manim},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/ManimCommunity/manim_editor}
}
@software{manimce,
author = {{The Manim Community Developers}},
license = {MIT},
month = {12},
title = {{Manim Mathematical Animation Framework}},
url = {https://www.manim.community/},
version = {v0.17.2},
year = {2022}
}

174
paper/paper.md Normal file
View File

@ -0,0 +1,174 @@
---
title: 'Manim Slides: A Python package for presenting Manim content anywhere'
tags:
- Python
- manim
- animations
- teaching
- conference presentations
- tool
authors:
- name: Jérome Eertmans
orcid: 0000-0002-5579-5360
affiliation: 1
affiliations:
- name: ICTEAM, UCLouvain, Belgium
index: 1
date: 2 March 2023
bibliography: paper.bib
---
# Summary
Manim Slides is a Python package that makes presenting Manim animations
straightforward. With minimal changes required to pre-existing code, one can
slide through animations in a *PowerPoint-like* manner, or share its slides
*online* using ReavealJS' power.
# Introduction
Presenting educational content has always been a difficult task, especially
when it uses temporal or iterative concepts. During the last decades, the
presence of computers in classrooms for educational purposes has increased
enormously, allowing teachers to show animated or interactive content.
With the democratization of YouTube, many people have decided to use this
platform to share educational content. Among these people, Grant Sanderson, a
YouTuber presenting videos on the theme of mathematics, quickly became known
for his original and quality animations. In 2018, Grant announced in a video
that he creates his animations using a self-developed Python tool called Manim
[@manim-announcement]. In 2019, he made the Manim source code public [@manimgl],
so anyone can use his tool. Very quickly, the community came together and, in
2020, created a "fork" of the original GitHub repository [@manimce], in order to
create a more accessible and better documented version of this tool. Since then,
the two versions are differentiated by using ManimGL for Grant Sanderson's
version, as it uses OpenGL for rendering, and ManimCE for the community edition
(CE).
Despite the many advantages of the Manim tool in terms of illustrating
mathematical concepts, one cannot help but notice that most presentations,
whether in the classroom or at a conference, are mainly done with PowerPoint
or PDF slides. One of the many advantages of these formats, as opposed to videos
created with Manim, is the ability to pause, rewind, etc., whenever you want.
To face this problem, in 2021, the manim-presentation tool was created
[@manim-presentation]. This tool allows you to present Manim animations as you
would present slides: with pauses, slide jumps, etc. However, this tool has
evolved very little since its inception and does not work with ManimGL.
In 2022, Manim Slides has been created from manim-presentation, with the aim
to make it a more complete tool, better documented, and usable on all platforms
and with ManimCE or ManimGL. After almost a year of existence, Manim Slides has
evolved a lot (see
[comparison section](#comparison-with-manim-presentation)),
has built a small community of contributors, and continues to
provide new features on a regular basis.
# Easy to Use Commitment
Manim Slides is commited to be an easy-to-use tool, with minimal installation
procedure and few modifications required. It can either be used locally with its
graphical user interface (GUI), or shared via one of the two formats it can
convert to:
* an HTML page thanks to the RevealJS Javascript package [@revealjs];
* or a PowerPoint (`.pptx`) file.
This work has a very similar syntax to Manim and offers a comprehensive
documentation hosted on [GitHub pages](https://jeertmans.github.io/manim-slides/), see
\autoref{fig:docs}.
![Manim Slides' documentation homepage.\label{fig:docs}](docs.png)
# Example usage
We have used manim-presentation for our presentation at the COST
Interact, hosted in Lyon, 2022, and
[available online](https://web.archive.org/web/20230507184944/https://eertmans.be/posts/cost-interact-presentation/).
This experience highly motivated the development of Manim Slides, and our
EuCAP 2023 presentation slides are already
[available online](https://web.archive.org/web/20230507211243/https://eertmans.be/posts/eucap-presentation/), thanks
to Manim Slides' HTML feature.
Also, one of our users created a short
[video tutorial](https://www.youtube.com/watch?v=Oc9g89VzKsY&ab_channel=TheoremofBeethoven)
and posted it on YouTube.
# Stability and releases
Manim Slides is continously tested on most recent Python versions, both ManimCE
and ManimGL, and on all major platforms: **Ubuntu**, **macOS** and **Windows**. Due to Manim
Slide's exposed API being very minimal, and the variety of tests that are
performed, this tool can be considered stable over time.
New releases are very frequent, as they mostly introduce enhancements or small
documention fixes, and the command-line tool automatically notifies for new
available updates. Therefore, regularly updating Manim Slides is highly
recommended.
# Statement of Need
Similar tools to Manim Slides also exist, such as its predecessor,
manim-presentation [@manim-presentation], or the web-based alternative, Manim
Editor [@manim-editor], but none of them provide the documentation level nor the
amount of features that Manim Slides strives to. This work makes the task of
presenting Manim content in front of an audience much easier than before,
allowing presenters to focus more on the content of their slides, rather than on
how to actually present them efficiently.
## Target Audience
Manim Slides was developed with the goal of making educational content more
accessible than ever. We believe that researchers, professors, teaching
assistants and anyone else who needs to teach scientific content can benefit
from using this tool. The ability to pace your presentation yourself is
essential, and Manim Slides gives you that ability.
## A Need for Portability
One of the major concerns with presenting content in a non-standard format
(i.e., not just a plain PDF) is the issue of portability.
Depending on the programs available, the power of the target computer,
or the access to the internet, not all solutions are equal.
From the same configuration file, Manim Slides offers a series of solutions to
share your slides, which we discuss on our
[Sharing your slides](https://jeertmans.github.io/manim-slides/reference/sharing.html)
page.
## Comparison with manim-presentation
Starting from [@manim-presentation]'s original work, Manim Slides now provides
numerous additional features.
A non-exhaustive list of those new features is as follows:
* ManimGL compatibility;
* playing slides in reverse;
* exporting slides to HTML and PowerPoint;
* 3D scene support;
* multiple key inputs can map to the same action
(e.g., useful when using a pointer);
* optionally hiding mouse cursor when presenting;
* recording your presentation;
* multiple video scaling methods (for speed-vs-quality tradeoff);
* and automatic detection of some scene parameters
(e.g., resolution or background color).
The complete and up-to-date set of features Manim Slide supports is
available in the
[online documentation](https://jeertmans.github.io/manim-slides/).
For new feature requests, we highly encourage users to
[create an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
with the appropriate template.
# Acknowledgements
We acknowledge the work of [@manim-presentation] that paved the initial structure
of Manim Slides with the manim-presentation Python package.
We also acknowledge Grant Sanderson for his tremendous work on Manim, as
well as the Manim Community contributors.
Finally, we also acknowledge contributions from the GitHub contributors on the
Manim Slides repository.
# References

1480
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,28 +10,10 @@ profile = "black"
py_version = 38
[tool.mypy]
check-untyped-defs = true
# Disallow dynamic typing
disallow-any-generics = true
disallow-incomplete-defs = true
disallow-subclassing-any = true
# Disallow untyped definitions and calls
disallow-untyped-defs = true
ignore-missing-imports = true
install-types = true
# None and optional handling
no-implicit-optional = true
no-warn-return-any = true
non-interactive = true
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
# Strict equality
strict-equality = true
warn-no-return = true
warn-redundant-casts = true
# Config file
warn-unused-configs = true
# Configuring warnings
warn-unused-ignores = true
strict = true
[tool.poetry]
authors = [
@ -61,19 +43,28 @@ packages = [
]
readme = "README.md"
repository = "https://github.com/jeertmans/manim-slides"
version = "4.8.3"
version = "4.13.2"
[tool.poetry.dependencies]
click = "^8.1.3"
click-default-group = "^1.2.2"
lxml = "^4.9.2"
manim = {version = "^0.17.0", optional = true}
manimgl = {version = "^1.6.1", optional = true}
numpy = "^1.19"
opencv-python = "^4.6.0.66"
pydantic = "^1.10.2"
pyside6 = "^6.4.1"
python = ">=3.8.1,<3.12"
python-pptx = "^0.6.21"
requests = "^2.28.1"
rich = "^13.3.2"
tqdm = "^4.64.1"
[tool.poetry.extras]
manim = ["manim"]
manimgl = ["manimgl"]
[tool.poetry.group.dev.dependencies]
black = "^22.10.0"
bump2version = "^1.0.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -1,17 +1,29 @@
# flake8: noqa: F403, F405
# type: ignore
import os
from manim import *
THEME = os.environ.get("MANIM_SLIDES_THEME", "light").lower().replace("_", "-")
class ManimSlidesLogo(Scene):
def construct(self):
tex_template = TexTemplate()
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
self.camera.background_color = "#ffffff"
self.camera.background_color = {
"light": "#ffffff",
"dark-docs": "#131416",
"dark-github": "#0d1117",
}[THEME]
logo_green = "#87c2a5"
logo_blue = "#525893"
logo_red = "#e07a5f"
logo_black = "#343434"
logo_black = {
"light": "#343434",
"dark-docs": "#d0d0d0",
"dark-github": "#c9d1d9",
}[THEME]
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)

BIN
static/logo_dark_docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
static/logo_dark_github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

21
static/make_logo.sh Executable file
View File

@ -0,0 +1,21 @@
#! /bin/bash
MANIM_SLIDES_THEME=light poetry run manim render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo.png
ln -f -r -s static/logo.png docs/source/_static/logo.png
MANIM_SLIDES_THEME=dark_docs poetry run manim render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_docs.png
ln -f -r -s static/logo_dark_docs.png docs/source/_static/logo_dark_docs.png
MANIM_SLIDES_THEME=dark_github poetry run manim render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_github.png
ln -f -r -s static/logo_dark_github.png docs/source/_static/logo_dark_github.png
MANIM_SLIDES_THEME=light poetry run manim render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_light_transparent.png
ln -f -r -s static/logo_light_transparent.png docs/source/_static/logo_light_transparent.png
MANIM_SLIDES_THEME=dark_docs poetry run manim render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_transparent.png
ln -f -r -s static/logo_dark_transparent.png docs/source/_static/logo_dark_transparent.png

BIN
static/wizard_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
static/wizard_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB