Compare commits

..

176 Commits

Author SHA1 Message Date
5461a20257 chore(version): bump 4.16.0 to 4.16.1 2023-08-24 13:16:13 +02:00
5e3603b40b fix(lib): correctly format enums on Python>=3.11
Closes #256

fix(tests): update tests and fix

chore(lib): simplify fix and more tests

chore(docs): document patch
2023-08-24 13:14:27 +02:00
d056d8d8b1 chore(version): bump 4.15.0 to 4.16.0 2023-08-08 14:45:03 +02:00
c95929dc7d fix(ci): version bump 2023-08-08 14:44:59 +02:00
e08edb6fe1 chore(docs): make 2nd example more different 2023-08-08 14:30:43 +02:00
efc3017df8 chore(paper): add paper's doi, update badges and add CFF 2023-08-08 14:13:29 +02:00
788727ea22 Merge branch 'optional' into main 2023-08-08 10:47:37 +02:00
0bb0285b18 fix(docs): typo in magics name 2023-08-08 10:46:44 +02:00
ce878bece2 [pre-commit.ci] pre-commit autoupdate (#239)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.281 → v0.0.282](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.281...v0.0.282)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-08 09:47:39 +02:00
1275e9119f chore(deps): make all groups optional (#238)
* chore(deps): make all groups optional

* chore(deps): update lock file

* chore(ci): run apt-get update
2023-08-07 18:25:06 +02:00
5555ac2e54 chore(ci): run apt-get update 2023-08-07 18:14:42 +02:00
838de83c75 chore(deps): update lock file 2023-08-07 18:07:22 +02:00
f1f7146c7e chore(deps): make all groups optional 2023-08-07 18:06:12 +02:00
fb02764bb7 feat(lib): add Jupyter magic (#237)
* feat(lib): add Jupyter magic

And also use the same logger level as manim (by default)

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

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

* fix(lib): remove deleted module

* chore(lib): fix typing issues

* chore(docs): document magic

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

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

* fix(ci): install kernel

* fix(ci): spawning is not necessary (and fails)

* chore(ci): add ipykernel

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-07 17:47:03 +02:00
f6f2e4090f fix(lib): fix import * (typo in name) 2023-08-07 10:11:16 +02:00
146a2f7839 chore(paper): fix brackets
As proposed by @labarba
2023-08-06 21:38:08 +02:00
d282766f2d feat(convert): support base64 encoded videos (#236)
* feat(convert): support base64 encoded videos

Thanks to @t-fritsch, Manim Slides can now convert to a fully self-contained HTML presentation using base64 encoded videos!

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

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

* chore(lib): explicit decode type

* feat(lib): auto detect mime type

* [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-08-06 19:06:43 +02:00
dec2f5e724 chore(docs): actually apply my own recomm. in example :-) 2023-08-04 16:49:37 +02:00
4d44166677 chore(version): bump 4.14.0 to 4.15.0 2023-08-04 16:17:50 +02:00
455f104a11 fix(present): finish up state machine (#234) 2023-08-04 16:17:21 +02:00
0f9048a27b fix(present): finish up state machine 2023-08-04 16:03:52 +02:00
9e1e0f2367 fix(lib): correctly caching and simplified structure (#233) 2023-08-04 13:37:27 +02:00
097ae8ffdd feat(present): fix presentation size and add screen option (#232)
* feat(present): fix presentation size and add screen option

This kinda fixes the issue of weird resizes between two presentations

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

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

* fix(ci): typing

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-04 13:33:21 +02:00
aace5dea11 fix(lib): correctly caching and simplified structure 2023-08-04 13:32:51 +02:00
738c54f9a6 chore(ci): ignore type 2023-08-04 11:15:38 +02:00
f01f811639 chore(present): inform about SIGINT 2023-08-04 11:14:24 +02:00
94265f6842 chore(present): warn about old configuration file 2023-08-04 10:46:35 +02:00
6ed76ffd01 fix(lib): correct key attribute 2023-08-04 10:45:19 +02:00
71df62d79b chore(docs): line length too long 2023-08-04 09:55:01 +02:00
fc36909688 chore(tests): zoom example 2023-08-04 09:54:42 +02:00
580da4a885 chore(docs): add some more indications 2023-08-02 16:59:02 +02:00
0de877e43a fix(docs): typo in RST 2023-08-02 16:48:59 +02:00
7313e3e0d4 feat(lib): add zoom animation (#230)
* feat(lib): add zoom animation

* chore(lib): fix zoom animation
2023-08-02 13:47:38 +02:00
1e967894db fix(lib): check if reversed file exist (#231)
This fixes a bug that would block the process forever because FFMPEG was asking if it could overwrite or not.
2023-08-02 12:54:00 +02:00
98fa5349d3 [pre-commit.ci] pre-commit autoupdate (#229)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.280 → v0.0.281](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.280...v0.0.281)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-01 20:56:40 +02:00
0e3ed3f9eb chore(deps): bump certifi from 2023.5.7 to 2023.7.22 (#228)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 09:33:12 +02:00
a45242236d chore(docs): add sphinx extension link 2023-07-25 14:39:47 +02:00
66451473b8 [pre-commit.ci] pre-commit autoupdate (#226)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.278 → v0.0.280](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.278...v0.0.280)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-25 10:29:38 +02:00
72152bd625 docs: fixed typo (#227)
Change link titled "HTML Presenetation" (typo) to "HTML Presentation".
2023-07-25 10:29:21 +02:00
88bffe0f0b chore(lib): remove all os.path in favor to pathlib (#225)
* chore(lib): remove all os.path in favor to pathlib

* fix(lib): str to path
2023-07-24 15:53:30 +02:00
a7719dbb8b feat(lib): change config file from json to toml (#224)
This PR introduces a **BREAKING CHANGE**: the general config file is now written in the TOML format. Keys are also placed under a shared subsection. Previous config files will be ignore.

Migration from the previous format can be easily performed with `manim-slides init` and copy/pasting the key codes if necessary.
2023-07-24 14:46:15 +02:00
529a6c534f fix(lib/cli): relative paths, empty slides, and tests (#223)
* fix(lib/cli): relative paths, empty slides, and tests

This fixes two issues:

1. Empty slides are now reported as error, to prevent indexing error;
2. Changing the folder path will now produce an absolute path to slides, which was not the case before and would lead to a "file does not exist error".

A few tests were also added to cover those

* fix(lib): fix from_file, remove useless field, and more

* chore(tests): remove print

* [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-07-24 13:58:54 +02:00
2b6240c4d3 chore(tests): test commons and defaults (#222)
Add new tests to improve overall coverage
2023-07-24 11:28:40 +02:00
d98d41aaa8 fix(lib/cli): fix validation error (#221) 2023-07-24 10:38:35 +02:00
d892a4e77d chore(docs): add fadeout 2023-07-20 17:36:55 +02:00
8069ab5405 chore(docs): improve example animation 2023-07-20 17:35:36 +02:00
f9e22fe63c chore(tests): improve test coverage (#220)
* chore(tests): improve test coverage

* chore(ci): fix display connection for manimgl

* chore(ci): same but for code coverage
2023-07-20 16:17:51 +02:00
b195b823ba chore(docs): improve directive's documentation 2023-07-20 13:59:28 +02:00
fc8717fa9c chore(docs): select docs members 2023-07-20 13:48:54 +02:00
dbbd6813ec feat(lib): add canvas and features (#219)
* feat(lib): add canvas and features

Add canvas to slide

* [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-07-20 13:38:18 +02:00
a10902eaaa feat(lib): add wait-time-between-slides attribute (#218)
Add a new attribute that will allow to automatically call `self.wait(...)` between two slides, to fix graphical issues.
2023-07-20 12:02:59 +02:00
f4c1c34994 feat(lib): improve wipe animation (#217)
* feat(lib): improve wipe animation

Improve wipe transition by using FadeIn and FadeOut. The also allows to support introducers and removers.

* [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-07-20 11:27:15 +02:00
540c7034c8 feat(lib/docs): add sphinx directive (#216)
This PR adds a new Sphinx directive, inspired by the Manim one. This directive is also used within the docs to display examples.
2023-07-19 17:11:58 +02:00
86aeeb861b feat(lib): add wip transition (#215) 2023-07-19 15:13:08 +02:00
979e2c549a [pre-commit.ci] pre-commit autoupdate (#212)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.9.0 → v2.10.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.9.0...v2.10.0)
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
- [github.com/astral-sh/ruff-pre-commit: v0.0.276 → v0.0.278](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.276...v0.0.278)

* chore(ci): apply ruff's suggestion

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2023-07-19 12:20:01 +02:00
4e4e29380b chore(lib): fix typo in warning message (#214) 2023-07-19 12:19:27 +02:00
caa4c48fe7 chore(deps): bump actions/upload-pages-artifact from 1 to 2 (#213)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 1 to 2.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 12:02:31 +02:00
a353de270e chore(docs): fix typo 2023-07-07 17:18:55 +02:00
60f284e748 Update index.md (#210)
Small typo
2023-07-06 23:28:21 +02:00
501af3b658 chore(README): update token 2023-07-05 14:50:32 +02:00
f820819896 chore(ci): add coverage report 2023-07-05 14:46:42 +02:00
9279d2a22a chore(deps): bumping pydantic to V2 (#207)
* chore(deps): bumping pydantic to V2

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

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

* fix(lib): validators

* fix(ci): add tests and fixes

* fix(lib): add missing mode arg

* fix(lib): change function name

* chore(tests): add more tests and use pytest

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

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

* chore(deps): change test deps

* chore(ci): install manim deps

* fix(ci): move to right place

* fix(lib): add custom schema

* fix(lib): validators

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-05 14:35:58 +02:00
e1d5fb732c [pre-commit.ci] pre-commit autoupdate (#206)
updates:
- https://github.com/charliermarsh/ruff-pre-commithttps://github.com/astral-sh/ruff-pre-commit
- [github.com/astral-sh/ruff-pre-commit: v0.0.275 → v0.0.276](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.275...v0.0.276)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-05 10:05:55 +02:00
384af332d8 chore(deps): bump actions/checkout from 1 to 3 (#203)
Bumps [actions/checkout](https://github.com/actions/checkout) from 1 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-29 14:02:21 +02:00
c231f4d100 [pre-commit.ci] pre-commit autoupdate (#204)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.272 → v0.0.275](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.272...v0.0.275)
- [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.4.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-29 14:02:12 +02:00
751eae74e9 chore(deps): bump actions/upload-artifact from 1 to 3 (#201)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 10:27:41 +02:00
97fe80caa2 chore(deps): bump actions/configure-pages from 2 to 3 (#200)
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 2 to 3.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 10:27:29 +02:00
a7eea6fbea [pre-commit.ci] pre-commit autoupdate (#199)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.270 → v0.0.272](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.270...v0.0.272)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-13 10:27:16 +02:00
04e2f265f6 chore(deps): bump actions/deploy-pages from 1 to 2 (#202)
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 1 to 2.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 10:27:02 +02:00
8096636cf1 chore(version): bump 4.13.2 to 4.14.0 2023-06-12 14:40:38 +02:00
c7e38bfb38 feat(convert): add to PDF conversion (#197)
* feat(convert): add to PDF conversion

Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page.

Closes #196

* chore(ci): remove experimental installer

* feat(convert): add to PDF conversion

Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page.

Closes #196

* feat(convert): add to PDF conversion

Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page.

Closes #196

* chore(deps): update lockfile
2023-06-12 14:28:20 +02:00
421cad3038 chore(deps): bump requests from 2.29.0 to 2.31.0 (#190)
Bumps [requests](https://github.com/psf/requests) from 2.29.0 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.29.0...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 13:53:31 +02:00
9edf23856c chore(deps): upgrading to qt6.5 (#198)
* chore(deps): upgrading to qt6.5

This PR proposes to update to PySide6.5.

On Ubuntu, you may face this problem (solution included): https://bugreports.qt.io/browse/PYSIDE-2306

* chore(ci): remove experimental installer
2023-06-12 13:46:15 +02:00
62236f5796 [pre-commit.ci] pre-commit autoupdate (#195)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.269 → v0.0.270](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.269...v0.0.270)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-09 11:33:48 +02:00
1e28d70c0e chore(README): add zenodo badge 2023-05-25 18:00:11 +02:00
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
35195f89e4 chore(version): bump 4.8.2 to 4.8.3 (#119)
* chore(version): bump 4.8.2 to 4.8.3

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

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

* chore(ci): always use apt-get update

See comment https://github.com/actions/runner-images/issues/6488#issuecomment-1295789463

* chore(docs): add opengraph image and update description

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-01 11:29:00 +01:00
2d7a67addf chore(license): change to MIT (#118)
* chore(license): change to MIT

After acceptance from owner of manim-presentation (via Telegram), I am changing the license to MIT.

* chore(ci): change license info
2023-01-31 17:58:13 +01:00
5b212ecccb fix(docs): add missing argument 2023-01-31 10:44:55 +01:00
f15a3e9b59 chore(version): bump to 4.8.2 2023-01-31 10:41:55 +01:00
205972125c chore(docs): improving the docs a bit more (#116)
* chore(docs): improving the docs a bit more

This adds some useful extensions, links, and add. information.

* fix(deps): update lockfiles no-cache
2023-01-31 10:29:21 +01:00
e9d28dc0a8 feat(lib): remove constraint on last animation and update readme (#117)
This removes the constraint that required a Slide class to end with a animation. This was actually not needed, and could lead to confusion since `self.wait` is not an animation with ManimGL, but well with Manim.

This fix, however, still means that a calls to `self.wait` with ManimGL, after last `self.pause` call, will be ignored.
2023-01-31 10:07:59 +01:00
70b5ee39c3 chore(docs): fix missing image, app. order and link 2023-01-30 22:45:43 +01:00
616e025867 fix(ci): missing indent 2023-01-30 22:25:40 +01:00
0ce4c18519 chore(docs): improving the docs (#115)
* chore(docs): improving the docs

This improves the docs as suggested by some reddit user. Now, most of Manim is documented in one place

* try: update lockfile

* chore(deps): remove duplicate key

* fix(ci): isort issue

See https://github.com/home-assistant/core/issues/86892

* fix(ci): bad identation

* fix(ci): fixing lock file

* fix(example): issue when ManimGL does not count wait as animation
2023-01-30 22:23:53 +01:00
68ff5269eb chore(ci): add yaml and toml formatters (#114)
* chore(ci): add yaml and toml formatters

* fix(ci): do not format poetry.lock

* chore(lib): update poetry.lock
2023-01-30 19:11:58 +01:00
753f4e788b chore(ci): improve docs build (#113)
* chore(ci): improve docs build

* fix(ci): trying to fix cache dir

* chore(ci): split caching in two steps
2023-01-30 14:05:31 +01:00
f1f98bf241 fix(ci): build docs using poetry env (#112) 2023-01-30 09:27:15 +01:00
4b413c1528 [pre-commit.ci] pre-commit autoupdate (#110)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.223 → v0.0.230](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.223...v0.0.230)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-24 19:01:34 +01:00
478e1d7d76 [pre-commit.ci] pre-commit autoupdate (#108)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.219 → v0.0.223](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.219...v0.0.223)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-17 07:02:23 +01:00
2b224530ab chore(lint): remove flake8 in favor to ruff (#103)
* chore(lint): apply suggested for flake8-black compat.

This applies the changes suggested by the Black documentation.

* from flake8 to ruff
2023-01-12 17:42:10 +01:00
cd7a054cf1 chore(version): bump to 4.8.1 (#102) 2023-01-01 19:27:53 +01:00
1ff2330ff2 fix(convert): correctly quote paths when using ffmpeg on Windows
With the current version of ffmpeg on Windows, the list file must enclosed by single quotes.
2023-01-01 19:12:45 +01:00
1e150bbb84 [pre-commit.ci] pre-commit autoupdate (#100)
updates:
- [github.com/pycqa/isort: v5.11.3 → 5.11.4](https://github.com/pycqa/isort/compare/v5.11.3...5.11.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-27 08:47:36 +01:00
13f19649aa [pre-commit.ci] pre-commit autoupdate (#99)
updates:
- [github.com/pycqa/isort: 5.11.0 → v5.11.3](https://github.com/pycqa/isort/compare/5.11.0...v5.11.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-20 11:09:07 +01:00
4c97bdd3a3 [pre-commit.ci] pre-commit autoupdate (#97)
updates:
- [github.com/pycqa/isort: 5.10.1 → 5.11.0](https://github.com/pycqa/isort/compare/5.10.1...5.11.0)
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-14 08:51:54 +01:00
777ff444a3 chore(cli): clean debug print 2022-12-09 09:48:36 +01:00
9cb1c35f00 chore(ci): move mypy config to pyproject.toml (#94) 2022-12-08 11:53:56 +01:00
1fed193cb3 chore(ci): move black config to pyproject.toml (#93)
* chore(ci): move black config to pyproject.toml

* chore(deps): update target version
2022-12-08 11:34:20 +01:00
9f227936f7 chore(lib): add link for audio blocking 2022-12-08 11:19:47 +01:00
2fe6139d18 chore(ci): move isort config to pyproject.toml (#92)
* chore(ci): move isort config to pyproject.toml

* fix(ci): typo in config
2022-12-08 11:16:54 +01:00
54f2c60c4e chore(deps): use carret version requirements (#91) 2022-12-08 10:25:16 +01:00
9810425ff2 chore(version): update version 2022-12-07 17:19:19 +01:00
3dc543e3a6 chore(docs): update README and fix typo 2022-12-07 17:17:07 +01:00
c0c73ad4d4 feat(cli): feally featured RevealJS template (#80)
* feat(cli): feally featured RevealJS template

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

RevealJS version is bumped to latest (4.4.0).

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

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

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

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

* fix(lib): pickling issue

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-07 16:27:23 +01:00
a82ca81dc5 chore(deps): add dependency groups (#89)
This uses poetry's dependency groups to be more rigorous about modules version.
2022-12-06 14:56:18 +01:00
a68a4e1517 feat(cli): speedup scaling with painter scaling (#88)
As suggested by PySide6's documentation, we avoid using pixmap.scaled() on every frame, but use scaled content to resize the image. This seems to ignore ratio, explaining why we need a new option flag.
2022-12-06 12:06:35 +01:00
96 changed files with 9892 additions and 3255 deletions

16
.bumpversion.cfg Normal file
View File

@ -0,0 +1,16 @@
[bumpversion]
current_version = 4.16.1
commit = True
message = chore(version): bump {current_version} to {new_version}
[bumpversion:file:manim_slides/__version__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
[bumpversion:file:pyproject.toml]
search = version = "{current_version}"
replace = version = "{new_version}"
[bumpversion:file:CITATION.cff]
search = version: v{current_version}
replace = version: v{new_version}

View File

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

View File

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

View File

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

View File

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

View File

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

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 }}

View File

@ -9,16 +9,16 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: CodeQL
on:
push:
branches: [ "main" ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
branches: [main]
schedule:
- cron: '45 3 * * 2'
- cron: 45 3 * * 2
jobs:
analyze:
@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
language: [python]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

48
.github/workflows/coverage.yml vendored Normal file
View File

@ -0,0 +1,48 @@
on: [push]
name: Code Coverage
jobs:
test:
name: Coverage
runs-on: ubuntu-latest
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: :99
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: poetry
- name: Install manim dependencies on Ubuntu
run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install xvfb on Ubuntu
run: |
sudo apt-get install xvfb
nohup Xvfb $DISPLAY &
- name: Install Manim Slides
run: |
poetry install --with test
- name: Run pytest and coverage
run: poetry run pytest --cov-report xml --cov=manim_slides tests/
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
fail_ci_if_error: true

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@v3
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

@ -8,8 +8,8 @@ jobs:
languagetool_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: reviewdog/action-languagetool@v1
with:
reporter: github-pr-review
level: warning
- uses: actions/checkout@v3
- uses: reviewdog/action-languagetool@v1
with:
reporter: github-pr-review
level: warning

View File

@ -4,7 +4,9 @@ name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
branches: [main]
pull_request:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@ -17,42 +19,78 @@ permissions:
# Allow one concurrent deployment
concurrency:
group: "pages"
group: pages
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
permissions: write-all
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Install Linux Dependencies
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install Python dependencies
run: pip install manim sphinx sphinx_click furo
- name: Install local Python package
run: pip install -e .
- name: Build animation and convert it into HTML slides
run: |
manim example.py ConvertExample
manim-slides convert ConvertExample docs/source/_static/slides.html -cembedded=true -ccontrols=true
- name: Build docs
run: cd docs && make html
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
# Upload docs/build/html dir
path: 'docs/build/html/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
- name: Checkout
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
cache: poetry
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install Linux Dependencies
run: |
sudo apt-get update
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Setup Pandoc
uses: nikeee/setup-pandoc@v1
- name: Install local Python package
run: poetry install --with docs
- name: Install IPython kernel
run: poetry run ipython kernel install --name "manim-slides" --user
- name: Restore cached media
id: cache-media-restore
uses: actions/cache/restore@v3
with:
path: media
key: ${{ runner.os }}-media
- name: Build animations
run: |
poetry run manim example.py ConvertExample BasicExample ThreeDExample
- 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
with:
path: media
key: ${{ steps.cache-media-restore.outputs.cache-primary-key }}
- name: Build docs
run: cd docs && poetry run make html
- name: Upload artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v2
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'
uses: actions/deploy-pages@v2

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]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Install build package
run: python -m pip install -U build
- name: Build wheels
run: python -m build --sdist
- uses: actions/upload-artifact@v2
with:
name: dist
path: dist/*.tar.*
release:
name: Release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
build_and_release:
name: Build and release
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: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: poetry
- name: Build wheels
run: poetry build
- 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,117 +0,0 @@
on:
pull_request:
paths:
- '**.py'
- '.github/workflows/test_examples.yml'
workflow_dispatch:
name: Test Examples
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: ":99"
jobs:
build-examples:
strategy:
fail-fast: false
matrix:
manim: [manim, manimgl]
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.8', '3.9', '3.10', '3.11']
exclude:
# excludes manimgl on Windows because if throws errors
# related to OpenGL, which seems hard to fix:
# Your graphics drivers do not support OpenGL 2.0.
- os: windows-latest
manim: manimgl
# We only test Python 3.11 on Windows and MacOS
- os: windows-latest
pyversion: '3.8'
- os: windows-latest
pyversion: '3.9'
- os: windows-latest
pyversion: '3.10'
manim: manim
- os: macos-latest
pyversion: '3.8'
- os: macos-latest
pyversion: '3.9'
- os: macos-latest
pyversion: '3.10'
manim: manim
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
cache: 'poetry'
# Path related stuff
- name: Append to Path on MacOS
if: matrix.os == 'macos-latest'
run: |
echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
- name: Append to Path on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
- name: Append to Path on Windows
if: matrix.os == 'windows-latest'
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
# OS depedencies
- name: Install manim dependencies on MacOs
if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
run: brew install ffmpeg py3cairo
- name: Install manimgl dependencies on MacOS
if: matrix.os == 'macos-latest' && matrix.manim == 'manimgl'
run: brew install ffmpeg
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install manimgl dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: |
sudo apt-get install libpango1.0-dev ffmpeg freeglut3-dev
- name: Install xvfb on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: |
sudo apt-get install xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
# Install Manim Slides
- name: Install Manim Slides
run: |
poetry config experimental.new-installer false
poetry install
# Render slides
- name: Render slides
if: matrix.manim == 'manim'
run: poetry run manim -ql example.py Example ThreeDExample
- name: Render slides
if: matrix.manim == 'manimgl'
run: poetry run -v manimgl -l example.py Example ThreeDExample
# Play slides
- name: Test slides
run: poetry run manim-slides Example ThreeDExample --skip-all
# Test slides to html
- name: Test convert on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: |
poetry run manim -ql example.py ConvertExample
poetry run manim-slides convert --to=html ConvertExample index.html

169
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,169 @@
on:
pull_request:
workflow_dispatch:
name: Tests
jobs:
pytest:
strategy:
fail-fast: false
matrix:
pyversion: ['3.8', '3.9', '3.10', '3.11']
runs-on: ubuntu-latest
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: :99
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
cache: poetry
- name: Run apt-get update on Ubuntu
run: sudo apt-get update
- name: Install manim dependencies on Ubuntu
run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install xvfb on Ubuntu
run: |
sudo apt-get install xvfb
nohup Xvfb $DISPLAY &
- name: Install Manim Slides
run: |
poetry install --with test
- name: Run pytest
run: poetry run pytest -x
build-examples:
strategy:
fail-fast: false
matrix:
manim: [manim, manimgl]
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.8', '3.9', '3.10', '3.11']
exclude:
# excludes manimgl on Windows because if throws errors
# related to OpenGL, which seems hard to fix:
# Your graphics drivers do not support OpenGL 2.0.
- os: windows-latest
manim: manimgl
# We only test Python 3.11 on Windows and MacOS
- os: windows-latest
pyversion: '3.8'
- os: windows-latest
pyversion: '3.9'
- os: windows-latest
pyversion: '3.10'
manim: manim
- os: macos-latest
pyversion: '3.8'
- os: macos-latest
pyversion: '3.9'
- os: macos-latest
pyversion: '3.10'
manim: manim
runs-on: ${{ matrix.os }}
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: :99
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
cache: poetry
# Path related stuff
- name: Append to Path on MacOS
if: matrix.os == 'macos-latest'
run: |
echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
- name: Append to Path on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
- name: Append to Path on Windows
if: matrix.os == 'windows-latest'
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
# OS depedencies
- name: Install manim dependencies on MacOs
if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
run: brew install ffmpeg py3cairo
- name: Install manimgl dependencies on MacOS
if: matrix.os == 'macos-latest' && matrix.manim == 'manimgl'
run: brew install ffmpeg
- name: Run apt-get update on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install manimgl dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: |
sudo apt-get install libpango1.0-dev ffmpeg freeglut3-dev
- name: Install xvfb on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: |
sudo apt-get install xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
# Install Manim Slides
- name: Install Manim Slides
run: |
poetry install --extras ${{ matrix.manim }}
# Render slides
- name: Render slides
if: matrix.manim == 'manim'
run: poetry run manim -ql example.py BasicExample ThreeDExample
- name: Render slides
if: matrix.manim == 'manimgl'
run: poetry run -v manimgl -l example.py BasicExample ThreeDExample
# Play slides
- name: Test slides
run: poetry run manim-slides BasicExample ThreeDExample --skip-all
# Test slides to html
- name: Test convert on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: |
poetry run manim -ql example.py ConvertExample
poetry run manim-slides convert --to=html ConvertExample index.html

43
.gitignore vendored
View File

@ -1,25 +1,46 @@
# Python files
__pycache__/
/env
/tests
/build
/dist
*.egg-info/
# Manim files
images/
/media
/presentation
/.vscode
slides/
.manim-slides.json
docs/source/media/
# ManimGL files
videos/
images/
# Manim Slides files
.manim-slides.toml
slides/
!tests/slides/
slides_assets/
# Docs
docs/build/
docs/source/_static/slides_assets/
slides.html
docs/source/_static/slides.html
docs/source/reference/.ipynb_checkpoints/
docs/source/_static/basic_example_assets/
docs/source/_static/basic_example.html
docs/source/_static/three_d_example.html
docs/source/_static/three_d_example_assets/
docs/source/reference/media/
# JOSE Paper
paper/paper.pdf
paper/media/
# Others
coverage.xml

View File

@ -1,59 +1,34 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
args: ["--python-version", "37", "--profile", "black"]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
args: ["--target-version", "py37"]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- flake8-comprehensions
- flake8-tidy-imports
- flake8-typing-imports
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.991'
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
args:
- --install-types
- --non-interactive
- --ignore-missing-imports
# Disallow dynamic typing
- --disallow-any-generics
- --disallow-subclassing-any
# Disallow untyped definitions and calls
- --disallow-untyped-defs
- --disallow-incomplete-defs
- --check-untyped-defs
# None and optional handling
- --no-implicit-optional
# Configuring warnings
- --warn-unused-ignores
- --warn-no-return
- --no-warn-return-any
- --warn-redundant-casts
# Strict equality
- --strict-equality
# Config file
- --warn-unused-configs
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.10.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
- id: pretty-format-toml
exclude: poetry.lock
args: [--autofix]
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.282
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

32
CITATION.cff Normal file
View File

@ -0,0 +1,32 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: Manim Slides
message: A Python package for presenting Manim content anywhere
type: software
authors:
- name: Jérome Eertmans
orcid: 'https://orcid.org/0000-0002-5579-5360'
website: 'https://eertmans.be'
identifiers:
- type: doi
value: 10.21105/jose.00206
description: The paper presenting the software.
repository-code: 'https://github.com/jeertmans/manim-slides'
url: 'https://eertmans.be/manim-slides'
abstract: >-
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.
keywords:
- Education
- Math Animations
- Presentation Tool
- PowerPoint
- Python
license: MIT
version: v4.16.1

View File

@ -1,674 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
MIT License
Copyright (c) 2023 Jérome Eertmans
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

174
README.md
View File

@ -1,13 +1,22 @@
![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]
[![DOI][doi-badge]][doi-url]
[![JOSE Paper][jose-badge]][jose-url]
[![codecov][codecov-badge]][codecov-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)
@ -16,24 +25,34 @@ Tool for live presentations using either [Manim (community edition)](https://www
- [Usage](#usage)
* [Basic Example](#basic-example)
* [Key Bindings](#key-bindings)
* [Interactive Tutorial](#interactive-tutorial)
* [Other Examples](#other-examples)
- [Features and Comparison with Original manim-presentation](#features-and-comparison-with-original-manim-presentation)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
- [Contributing](#contributing)
* [Reporting an Issue](#reporting-an-issue)
* [Seeking for Help](#seeking-for-help)
* [Contact](#contact)
## Installation
<!-- start install -->
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a virtual environment (e.g., [venv](https://docs.python.org/3/tutorial/venv.html)) for a local installation.
### Dependencies
<!-- start deps -->
Manim Slides requires either Manim or ManimGL to be installed. Having both packages installed is fine too.
If none of those packages are installed, please refer to their specific installation guidelines:
- [Manim](https://docs.manim.community/en/stable/installation.html)
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
<!-- end deps -->
### Pip Install
The recommended way to install the latest release is to use pip:
@ -42,28 +61,34 @@ The recommended way to install the latest release is to use pip:
pip install manim-slides
```
### Install From Repository
An alternative way to install Manim Slides is to clone the git repository, and install from there:
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
git clone https://github.com/jeertmans/manim-slides
pip install -e .
pip install manim-slides[manim] # For Manim
# or
pip install manim-slides[manimgl] # For ManimGL
```
> *Note:* the `-e` flag allows you to edit the files, and observe the changes directly when using Manim Slides
[^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.
<!-- end install -->
## Usage
<!-- 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 command-line documentation is available [online](https://eertmans.be/manim-slides/).
The documentation is available [online](https://eertmans.be/manim-slides/).
### Basic Example
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
```python
@ -73,32 +98,28 @@ from manim import *
# or: from manimlib import *
from manim_slides import Slide
class Example(Slide):
class BasicExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
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.wait()
self.next_slide() # Waits user to press continue to go to the next slide
```
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(...)`.
First, render the animation files:
```bash
manim example.py
manim example.py BasicExample
# or
manimgl example.py
manimgl example.py BasicExample
```
To start the presentation using `Scene1`, `Scene2` and so on simply run:
@ -110,14 +131,20 @@ manim-slides [OPTIONS] Scene1 Scene2...
Or in this example:
```bash
manim-slides Example
manim-slides BasicExample
```
<!-- end usage -->
## Key Bindings
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:
@ -134,6 +161,12 @@ manim-slides init
> **_NOTE:_** `manim-slides` uses key codes, which are platform dependent. Using the configuration wizard is therefore highly recommended.
## Interactive Tutorial
Click on the image to watch a slides presentation that explains you how to use Manim Slides.
[![Manim Slides Docs](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/docs.png)](https://eertmans.be/manim-slides/)
## Other Examples
Other examples are available in the [`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py) file, if you downloaded the git repository.
@ -143,27 +176,21 @@ Below is a small recording of me playing with the slides back and forth.
![](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif)
## Features and Comparison with original manim-presentation
## Comparison with Similar Tools
Below is a non-exhaustive list of features:
There exists are variety of tools that allows to create slides presentations containing Manim animations.
| Feature | `manim-slides` | `manim-presentation` |
|:--------|:--------------:|:--------------------:|
| Support for Manim | :heavy_check_mark: | :heavy_check_mark: |
| Support for ManimGL | :heavy_check_mark: | :heavy_multiplication_x: |
| Configurable key bindings | :heavy_check_mark: | :heavy_check_mark: |
| Configurable paths | :heavy_check_mark: | :heavy_multiplication_x: |
| Play / Pause slides | :heavy_check_mark: | :heavy_check_mark: |
| Next / Previous slide | :heavy_check_mark: | :heavy_check_mark: |
| Replay slide | :heavy_check_mark: | :heavy_check_mark: |
| Reverse slide | :heavy_check_mark: | :heavy_multiplication_x: |
| Multiple key per actions | :heavy_check_mark: | :heavy_multiplication_x: |
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
| Hide mouse cursor | :heavy_check_mark: | :heavy_multiplication_x: |
Below is a comparison of the most used ones with Manim Slides:
| Project name | Manim Slides | Manim Presentation | Manim Editor | Jupyter Notebooks |
|:------------:|:------------:|:------------------:|:------------:|:-----------------:|
| Link | [![GitHub Repo stars](https://img.shields.io/github/stars/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/stars/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/stars/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/stars/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Activity | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Usage | Command-line | Command-line | Web Browser | Notebook |
| Note | Requires minimal modif. in scenes files | Requires minimal modif. in scenes files | Requires the usage of sections, and configuration through graphical interface | Relies on `nbconvert` to create slides from a Notebook |
| Support for ManimGL | Yes | No | No | No |
| Web Browser presentations | Yes | No | Yes | No |
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
## F.A.Q
@ -178,8 +205,67 @@ in *Settings*->*Display*.
## Contributing
Contributions are more than welcome!
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/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
[jose-url]: https://doi.org/10.21105/jose.00206
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
[codecov-url]: https://codecov.io/gh/jeertmans/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

3
docs/source/changelog.md Normal file
View File

@ -0,0 +1,3 @@
# Changelog
Changes between releases are listed in Manim Slides' [Github releases](https://github.com/jeertmans/manim-slides/releases). You can read the [latest release here](https://github.com/jeertmans/manim-slides/releases).

View File

@ -8,13 +8,35 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Manim Slides"
copyright = "2022, Jérome Eertmans"
copyright = "2023, Jérome Eertmans"
author = "Jérome Eertmans"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx.ext.autodoc", "sphinx_click"]
extensions = [
# Built-in
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
# Additional
"nbsphinx",
"myst_parser",
"sphinxext.opengraph",
"sphinx_click",
"sphinx_copybutton",
# Custom
"manim_slides.docs.manim_slides_directive",
]
typehints_defaults = "comma"
typehints_use_signature = True
typehints_use_signature_return = True
myst_enable_extensions = [
"colon_fence",
"html_admonition",
]
templates_path = ["_templates"]
exclude_patterns = []
@ -25,3 +47,36 @@ exclude_patterns = []
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",
"url": "https://github.com/jeertmans/manim-slides",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
],
"source_repository": "https://github.com/jeertmans/manim-slides/",
"source_branch": "main",
"source_directory": "docs/source/",
}
# -- Intersphinx mapping
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"manim": ("https://docs.manim.community/en/stable/", None),
"manimlib": ("https://3b1b.github.io/manim/", None),
}
# -- OpenGraph settings
ogp_site_url = "https://eertmans.be/manim-slides/"
ogp_use_first_image = True

View File

@ -0,0 +1,46 @@
# Contributing
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!
```{toctree}
:hidden:
workflow
internals
```
[Workflow](./workflow)
: how to work on this project. Start here if you're a new contributor.
[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

@ -0,0 +1,11 @@
# Internals
Manim-Slides' work in split in two steps: first, when rendering animation, and, second, when converting multiple animations into one slides presentation.
## Rendering
To render animations, Manim Slides simply uses Manim or ManimGL, and creates some additional output files that it needs for the presentation.
## Slides presentation
Manim Slides searches for the local artifacts it generated previously, and concatenates them into one presentation. For the graphical interface, it uses `PySide6`.

View File

@ -0,0 +1,83 @@
# Workflow
This document is there to help you recreate a working environment for Manim Slides.
## Dependencies
```{include} ../../../README.md
:start-after: <!-- start deps -->
:end-before: <!-- end deps -->
```
## Forking the repository and cloning it locally
We use GitHub to host Manim Slides' repository, and we encourage contributors to use git.
Useful links:
* [GitHub's Hello World](https://docs.github.com/en/get-started/quickstart/hello-world).
* [GitHub Pull Request in 100 Seconds](https://www.youtube.com/watch?v=8lGpZkjnkt4&ab_channel=Fireship).
Once you feel comfortable with git and GitHub, [fork](https://github.com/jeertmans/manim-slides/fork) the repository, and clone it locally.
As for every Python project, using virtual environment is recommended to avoid conflicts between modules. For Manim Slides, we use [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer). If not already, please install it.
## Installing Python modules
With Poetry, installation becomes straightforward:
```bash
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.
Instead, you either need to prepend `poetry run` to any command, e.g.:
```bash
poetry run manim-slides wizard
```
or enter a new shell that uses this new Python environment:
```
poetry shell
manim-slides wizard
```
## Testing your code
Most of the tests are done with GitHub actions, thus not on your computer. The only command you should run locally is `pre-commit run --all-files`: this runs a few linter and formatter to make sure the code quality and style stay constant across time. If a warning or an error is displayed, please fix it before going to next step.
## Proposing changes
Once you feel ready and think your contribution is ready to be reviewed, create a [pull request](https://github.com/jeertmans/manim-slides/pulls) and wait for a reviewer to check your work!
Many thanks to you!

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) | [`convert --to=pdf`](reference/cli.md)
| :--- | :---: | :---: | :---: | :---: |
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
| Replay slide | Yes | No | No | N/A |
| Pause animation | Yes | No | No | N/A |
| Play slide in reverse | Yes | No | No | N/A |
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
| Animation count | Yes | No | No | N/A |
| Needs Python with Manim Slides installed | Yes | No | No | No
| Requires internet access | No | Yes | No | No |
| Auto. play slides | Yes | Yes | Yes | N/A |
| Loops support | Yes | Yes | Yes | N/A |
| Fully customizable | No | Yes (`--use-template` option) | No | No |
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1] | None |
| Works cross-platforms | Yes | Yes | Partly[^1][^2] | Yes |
:::
[^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.

52
docs/source/index.md Normal file
View File

@ -0,0 +1,52 @@
---
hide-toc: true
og:description: Manim Slides makes creating slides with Manim super easy!
---
```{eval-rst}
.. 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
```
# Welcome to Manim Slide's documentation
Manim Slides makes creating slides with Manim super easy!
In a [very few steps](./quickstart), you can create slides and present them either using the GUI, or your browser.
Slide through the demo below to get a quick glimpse on what you can do with Manim Slides.
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="_static/slides.html"></iframe></div>
```{toctree}
:hidden:
quickstart
reference/index
features_table
```
```{toctree}
:caption: Development
:hidden:
contributing/index
changelog
license
```

View File

@ -1,28 +0,0 @@
.. manim-slides documentation master file, created by
sphinx-quickstart on Wed Sep 21 15:07:28 2022.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. image:: _static/logo.png
:width: 600px
:align: center
:alt: Manim Slide logo
Welcome to Manim Slide's CLI documentation!
===========================================
.. raw:: html
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="_static/slides.html"></iframe></div>
This page contains an exhaustive list of all the commands available with `manim-slides`.
If you need help installing or using Manim Slide, please refer to the `GitHub README <https://github.com/jeertmans/manim-slides>`_.
.. click:: manim_slides.__main__:cli
:prog: manim-slides
:nested: full

5
docs/source/license.md Normal file
View File

@ -0,0 +1,5 @@
# License
```{include} ../../LICENSE.md
```

21
docs/source/quickstart.md Normal file
View File

@ -0,0 +1,21 @@
# Quickstart
## Installation
```{include} ../../README.md
:start-after: <!-- start install -->
:end-before: <!-- end install -->
```
## Creating your first slides
```{include} ../../README.md
:start-after: <!-- start usage -->
:end-before: <!-- end usage -->
```
The output slides should look this this:
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="_static/basic_example.html"></iframe></div>
For more advanced examples, see the [Examples](reference/examples) section.

View File

@ -0,0 +1,27 @@
# Application Programming Interface
Manim Slides' API is very limited: it simply consists of two classes, `Slide`
and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
Therefore, 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:
add_to_canvas,
canvas,
canvas_mobjects,
end_loop,
mobjects_without_canvas,
next_slide,
pause,
remove_from_canvas,
start_loop,
wait_time_between_slides,
wipe,
zoom,
.. autoclass:: manim_slides.ThreeDSlide
:members:
```

View File

@ -0,0 +1,10 @@
# Command Line Interface
This page contains an exhaustive list of all the commands available with `manim-slides`.
```{eval-rst}
.. click:: manim_slides.__main__:cli
:prog: manim-slides
:nested: full
```

View File

@ -0,0 +1,130 @@
# Examples
Contents of `example.py`.
Do not forget to import Manim Slides and Manim or ManimGL:
```python
from manim import *
from manim_slides import Slide, ThreeDSlide
```
or
```python
from manimlib import *
from manim_slides import Slide, ThreeDSlide
```
Then, each presentation, named `SCENE`, was generated with those two commands:
```bash
manim example.py SCENE # or manimgl example SCENE
manim-slides convert SCENE scene.html -ccontrols=true
```
where `-ccontrols=true` indicates that we want to display the blue navigation arrows.
## Basic Example
Basic example from quickstart.
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="../_static/basic_example.html"></iframe></div>
```{eval-rst}
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:pyobject: BasicExample
```
## 3D Example
Example using 3D camera. As Manim and ManimGL handle 3D differently, definitions are slightly different.
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="../_static/three_d_example.html"></iframe></div>
### With Manim
```{eval-rst}
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:dedent: 4
:start-after: [manim-3d]
:end-before: [manim-3d]
```
### With ManimGL
```{eval-rst}
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:dedent: 4
:start-after: [manimgl-3d]
: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.
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="../_static/slides.html"></iframe></div>
```{eval-rst}
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:pyobject: ConvertExample
```

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

@ -0,0 +1,37 @@
# Reference Documentation
Automatically generated reference for Manim Slides.
```{toctree}
:hidden:
api
cli
examples
gui
html
IPython magic <ipython_magic>
sharing
Sphinx Extension <sphinx_extension>
```
[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.
[Examples](./examples): curated list of examples and their output.
[Graphical User Interface](./gui): details about the main Manim Slide' feature.
[HTML Presentation](./html): an alternative way of presenting your animations.
[IPython Magic](./ipython_magic): a magic to render and display Manim Slides inside notebooks.
+ [Example](./magic_example): example notebook using the magics.
[Sharing](./sharing): how to share your presentation with others.
[Sphinx Extension](./sphinx_extension): a Sphinx extension for diplaying Manim Slides animations within your documentation.

View File

@ -0,0 +1,6 @@
# Manim Slides' IPython magic
```{eval-rst}
.. automodule:: manim_slides.ipython.ipython_magic
:members: ManimSlidesMagic
```

View File

@ -0,0 +1,100 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "6896875b-34ce-4fc5-809c-669c295067e7",
"metadata": {},
"source": [
"# Jupyter Magic Example\n",
"\n",
"This small example shows how to use the Manim Slides cell (`%%manim_slides`) and line (`%manim_slides`) magics:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a73f1c06-c7f8-4f19-a90e-e283bfb8c7c5",
"metadata": {},
"outputs": [],
"source": [
"from manim import *\n",
"from manim_slides import *"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68dda1a0-74ff-4d9e-9575-5b25a98f21e7",
"metadata": {},
"outputs": [],
"source": [
"%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true\n",
"\n",
"config.media_embed = True\n",
"\n",
"class MySlide(Slide):\n",
" def construct(self):\n",
" square = Square()\n",
" circle = Circle()\n",
" \n",
" self.play(Create(square))\n",
" self.next_slide()\n",
" self.play(Transform(square, circle))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "54fa2d3e-bfee-417d-b64b-f3f30a8749ea",
"metadata": {},
"outputs": [],
"source": [
"class MyOtherSlide(Slide):\n",
" def construct(self):\n",
" text = VGroup(\n",
" Text(\"Press\"),\n",
" Text(\"and\"),\n",
" Text(\"loop\"),\n",
" ).arrange(DOWN, buff=1.)\n",
" \n",
" self.play(Write(text))\n",
" self.next_slide()\n",
" self.start_loop()\n",
" self.play(Indicate(text[-1], scale_factor=2., run_time=.5))\n",
" self.end_loop()\n",
" self.play(FadeOut(text))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7d8ad450-1487-4ca7-8d89-bf8ac344e1fa",
"metadata": {},
"outputs": [],
"source": [
"%manim_slides -v WARNING --progress_bar None MyOtherSlide --manim-slides controls=true"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"language": "python",
"name": "manim-slides"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,184 @@
# 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!
### Static PDF presentation
If you ever need backup slides, that are only made of PDF pages
with static images, you can generate such a PDF with the following command:
```bash
manim-slides convert --to=pdf BasicExample basic_example.pdf
```
Note that you will lose all the benefits from animated slides. Therefore,
this is only recommended to be used as a backup plan. By default, the last frame
of each slide will be printed. This can be changed to be the first one with
`-cframe_index=first`.

View File

@ -0,0 +1,6 @@
# Manim Slides' Sphinx directive
```{eval-rst}
.. automodule:: manim_slides.docs.manim_slides_directive
:members: ManimSlidesDirective
```

View File

@ -16,39 +16,64 @@ else:
from manim_slides import Slide, ThreeDSlide
class Example(Slide):
class BasicExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.pause()
self.next_slide() # Waits user to press continue to go to the next slide
self.start_loop()
self.start_loop() # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.end_loop() # This will loop until user inputs a key
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.next_slide() # Waits user to press continue to go to the next slide
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
class MultipleAnimationsInLastSlide(Slide):
"""This is used to check against solution for issue #161."""
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))
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):
"""WARNING: this example does not seem to work with ManimGL."""
def tinywait(self):
self.wait(0.1)
def construct(self):
self.wait_time_between_slides = 0.1
title = VGroup(
Text("From Manim animations", t2c={"From": BLUE}),
@ -70,7 +95,7 @@ class ConvertExample(Slide):
self.play(FadeIn(title))
self.pause()
self.next_slide()
code = Code(
code="""from manim import *
@ -139,10 +164,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",
@ -161,7 +186,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",
@ -184,42 +209,33 @@ class Example(Slide):
language="console",
).shift(DOWN)
self.clear()
self.play(FadeIn(code))
self.tinywait()
self.pause()
self.play(self.wipe(title, code))
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)
@ -238,11 +254,9 @@ class Example(Slide):
self.play(Transform(dot, square))
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(
@ -254,13 +268,12 @@ class Example(Slide):
)
self.play(Transform(square, learn_more_text))
self.tinywait()
# For ThreeDExample, things are different
if not MANIMGL:
# [manim-3d]
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
@ -274,7 +287,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)
@ -284,19 +297,20 @@ 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]
else:
# [manimgl-3d]
# WARNING: 3b1b's manim change how ThreeDScene work,
# this is why things have to be managed differently.
class ThreeDExample(Slide):
@ -319,10 +333,13 @@ else:
)
self.play(GrowFromCenter(circle))
updater = lambda m, dt: m.increment_theta((75 * DEGREES / 4) * dt)
def updater(m, dt):
return 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)
@ -331,14 +348,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,62 @@
# 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)
elif name == "ManimSlidesMagic":
module = __import__(
"manim_slides.ipython.ipython_magic", None, None, ["ManimSlidesMagic"]
)
magic = getattr(module, name)
from IPython import get_ipython
ipy = get_ipython()
if ipy is not None:
ipy.register_magics(magic)
return magic
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__", "ManimSlidesMagic", "Slide", "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.7.1"
__version__ = "4.16.1"

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,
)
@ -43,8 +44,7 @@ def config_options(function: F) -> F:
def verbosity_option(function: F) -> F:
"""Wraps a function to add verbosity option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
def callback(ctx: Context, param: Parameter, value: str) -> 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,43 +1,63 @@
import os
import hashlib
import json
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 Any, Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel, root_validator, validator
import rtoml
from pydantic import (
BaseModel,
Field,
FilePath,
PositiveInt,
field_validator,
model_validator,
)
from pydantic_extra_types.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
"""Represents a list of key codes, with optionally a name."""
ids: Set[int]
ids: List[PositiveInt] = Field(unique=True)
name: Optional[str] = None
def set_ids(self, *ids: int) -> None:
self.ids = set(ids)
@field_validator("ids")
@classmethod
def ids_is_non_empty_set(cls, ids: Set[Any]) -> Set[Any]:
if len(ids) <= 0:
raise ValueError("Key's ids must be a non-empty set")
return ids
@validator("ids", each_item=True)
def id_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Key ids cannot be negative integers")
return v
def set_ids(self, *ids: int) -> None:
self.ids = list(set(ids))
def match(self, key_id: int) -> bool:
m = key_id in self.ids
@ -48,9 +68,7 @@ class Key(BaseModel): # type: ignore
return m
class Config(BaseModel): # type: ignore
"""General Manim Slides config"""
class Keys(BaseModel): # type: ignore
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
CONTINUE: Key = Key(ids=[Qt.Key_Right], name="CONTINUE / NEXT")
BACK: Key = Key(ids=[Qt.Key_Left], name="BACK")
@ -59,28 +77,47 @@ class Config(BaseModel): # type: ignore
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
@root_validator
@model_validator(mode="before")
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
ids: Set[int] = set()
for key in values.values():
if len(ids.intersection(key.ids)) != 0:
if len(ids.intersection(key["ids"])) != 0:
raise ValueError(
"Two or more keys share a common key code: please make sure each key has distinct key codes"
)
ids.update(key.ids)
ids.update(key["ids"])
return values
def merge_with(self, other: "Config") -> "Config":
def merge_with(self, other: "Keys") -> "Keys":
for key_name, key in self:
other_key = getattr(other, key_name)
key.ids.update(other_key.ids)
key.ids = list(set(key.ids).union(other_key.ids))
key.name = other_key.name or key.name
return self
class Config(BaseModel): # type: ignore
"""General Manim Slides config"""
keys: Keys = Keys()
@classmethod
def from_file(cls, path: Path) -> "Config":
"""Reads a configuration from a file."""
return cls.model_validate(rtoml.load(path)) # type: ignore
def to_file(self, path: Path) -> None:
"""Dumps the configuration to a file."""
rtoml.dump(self.model_dump(), path, pretty=True)
def merge_with(self, other: "Config") -> "Config":
self.keys = self.keys.merge_with(other.keys)
return self
class SlideType(str, Enum):
slide = "slide"
loop = "loop"
@ -92,26 +129,27 @@ class SlideConfig(BaseModel): # type: ignore
start_animation: int
end_animation: int
number: int
terminated: bool = False
terminated: bool = Field(False, exclude=True)
@validator("start_animation", "end_animation")
@field_validator("start_animation", "end_animation")
@classmethod
def index_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
return v
@validator("number")
@field_validator("number")
@classmethod
def number_is_strictly_posint(cls, v: int) -> int:
if v <= 0:
raise ValueError("Slide number cannot be negative or zero")
return v
@root_validator
@model_validator(mode="before")
def start_animation_is_before_end(
cls, values: Dict[str, Union[SlideType, int, bool]]
) -> Dict[str, Union[SlideType, int, bool]]:
if values["start_animation"] >= values["end_animation"]: # type: ignore
if values["start_animation"] == values["end_animation"] == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
@ -138,61 +176,68 @@ class SlideConfig(BaseModel): # type: ignore
class PresentationConfig(BaseModel): # type: ignore
slides: List[SlideConfig]
files: List[str]
slides: List[SlideConfig] = Field(min_length=1)
files: List[FilePath]
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black"
@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?"
)
@classmethod
def from_file(cls, path: Path) -> "PresentationConfig":
"""Reads a presentation configuration from a file."""
with open(path, "r") as f:
obj = json.load(f)
if not os.path.isfile(v):
raise ValueError(f"Animation file {v} is not a file")
if files := obj.get("files", None):
# First parent is ../slides
# so we take the parent of this parent
parent = Path(path).parents[1]
for i in range(len(files)):
files[i] = parent / files[i]
return v
return cls.model_validate(obj) # type: ignore
@root_validator
def to_file(self, path: Path) -> None:
"""Dumps the presentation configuration to a file."""
with open(path, "w") as f:
f.write(self.model_dump_json(indent=2))
@model_validator(mode="after")
def animation_indices_match_files(
cls, values: Dict[str, Union[List[SlideConfig], List[str]]]
) -> Dict[str, Union[List[SlideConfig], List[str]]]:
files = values.get("files")
slides = values.get("slides")
if files is None or slides is None:
return values
cls, config: "PresentationConfig"
) -> "PresentationConfig":
files = config.files
slides = config.slides
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
return config
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,15 +247,23 @@ 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.writelines(f"file '{path.absolute()}'\n" for path in files)
f.close()
command = [
FFMPEG_BIN,
command: List[str] = [
str(FFMPEG_BIN),
"-f",
"concat",
"-safe",
@ -219,7 +272,7 @@ class PresentationConfig(BaseModel): # type: ignore
f.name,
"-c",
"copy",
dest_path,
str(dest_path),
"-y",
]
logger.debug(" ".join(command))
@ -234,18 +287,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,77 @@
import mimetypes
import os
import platform
import subprocess
import sys
import tempfile
import webbrowser
from base64 import b64encode
from enum import Enum
from typing import Any, Callable, Dict, Generator, List, Type
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
from lxml import etree
from PIL import Image
from pydantic import (
BaseModel,
ConfigDict,
FilePath,
GetCoreSchemaHandler,
PositiveFloat,
PositiveInt,
ValidationError,
)
from pydantic_core import CoreSchema, core_schema
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
DATA_URI_FIX = r"""
// Fix found by @t-fritsch on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
function fixBase64VideoBackground(event) {
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
if (event.currentSlide.getAttribute('data-background-video')) {
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
video = background.querySelector('video'),
sources = video.querySelectorAll('source');
sources.forEach((source, i) => {
const src = source.getAttribute('src');
if(src.match(/^data:video.*;base64$/)){
const nextSrc = sources[i+1]?.getAttribute('src');
video.setAttribute('src', `${src},${nextSrc}`);
}
});
}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
"""
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:
@ -31,32 +86,184 @@ def validate_config_option(
return config
def data_uri(file: Path) -> str:
"""
Reads a video and returns the corresponding data-uri.
"""
b64 = b64encode(file.read_bytes()).decode("ascii")
mime_type = mimetypes.guess_type(file)[0] or "video/mp4"
return f"data:{mime_type};base64,{b64}"
class Converter(BaseModel): # type: ignore
presentation_configs: List[PresentationConfig] = []
assets_dir: str = "{basename}_assets"
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
def open(self, file: str) -> bool:
def load_template(self) -> str:
"""Returns the template as a string.
An empty string is returned if no template is used."""
return ""
def open(self, file: Path) -> Any:
"""Opens a file, generated with converter, using appropriate application."""
return webbrowser.open(file)
raise NotImplementedError
@classmethod
def from_string(cls, s: str) -> Type["Converter"]:
"""Returns the appropriate converter from a string name."""
return {
"html": RevealJS,
"pdf": PDF,
"pptx": PowerPoint,
}[s]
class JSBool(str, Enum):
class Str(str):
"""A simple string, but quoted when needed."""
# This fixes pickling issue on Python 3.8
__reduce_ex__ = str.__reduce_ex__
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.str_schema()
def __str__(self) -> str:
"""Ensures that the string is correctly quoted."""
if self in ["true", "false", "null"]:
return self
else:
return f"'{super().__str__()}'"
class StrEnum(Enum):
def __str__(self) -> str:
return str(self.value)
Function = str # Basically, anything
class JsTrue(str, StrEnum):
true = "true"
class JsFalse(str, StrEnum):
false = "false"
class JsBool(Str, StrEnum): # type: ignore
true = "true"
false = "false"
class RevealTheme(str, Enum):
class JsNull(Str, StrEnum): # type: ignore
null = "null"
class ControlsLayout(Str, StrEnum): # type: ignore
edges = "edges"
bottom_right = "bottom-right"
class ControlsBackArrows(Str, StrEnum): # type: ignore
faded = "faded"
hidden = "hidden"
visibly = "visibly"
class SlideNumber(Str, StrEnum): # type: ignore
true = "true"
false = "false"
hdotv = "h.v"
handv = "h/v"
c = "c"
candt = "c/t"
class ShowSlideNumber(Str, StrEnum): # type: ignore
all = "all"
print = "print"
speaker = "speaker"
class KeyboardCondition(Str, StrEnum): # type: ignore
null = "null"
focused = "focused"
class NavigationMode(Str, StrEnum): # type: ignore
default = "default"
linear = "linear"
grid = "grid"
class AutoPlayMedia(Str, StrEnum): # type: ignore
null = "null"
true = "true"
false = "false"
PreloadIframes = AutoPlayMedia
class AutoAnimateMatcher(Str, StrEnum): # type: ignore
null = "null"
class AutoAnimateEasing(Str, StrEnum): # type: ignore
ease = "ease"
AutoSlide = Union[PositiveInt, JsFalse]
class AutoSlideMethod(Str, StrEnum): # type: ignore
null = "null"
MouseWheel = Union[JsNull, float]
class Transition(Str, StrEnum): # type: ignore
none = "none"
fade = "fade"
slide = "slide"
convex = "convex"
concave = "concave"
zoom = "zoom"
class TransitionSpeed(Str, StrEnum): # type: ignore
default = "default"
fast = "fast"
slow = "slow"
class BackgroundSize(Str, StrEnum): # 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
class Display(Str, StrEnum): # type: ignore
block = "block"
class RevealTheme(str, StrEnum):
black = "black"
white = "white"
league = "league"
@ -71,88 +278,323 @@ class RevealTheme(str, Enum):
class RevealJS(Converter):
background_color: str = "black"
controls: JSBool = JSBool.false
embedded: JSBool = JSBool.false
fragments: JSBool = JSBool.false
height: str = "100%"
loop: JSBool = JSBool.false
progress: JSBool = JSBool.false
reveal_version: str = "3.7.0"
# Export option: use data-uri
data_uri: bool = False
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
margin: float = 0.04
min_scale: float = 0.2
max_scale: float = 2.0
# Configuration options from RevealJS
controls: JsBool = JsBool.false
controls_tutorial: JsBool = JsBool.true
controls_layout: ControlsLayout = ControlsLayout.bottom_right
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
progress: JsBool = JsBool.false
slide_number: SlideNumber = SlideNumber.false
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
hash_one_based_index: JsBool = JsBool.false
hash: JsBool = JsBool.false
respond_to_hash_changes: JsBool = JsBool.false
history: JsBool = JsBool.false
keyboard: JsBool = JsBool.true
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
disable_layout: JsBool = JsBool.false
overview: JsBool = JsBool.true
center: JsBool = JsBool.true
touch: JsBool = JsBool.true
loop: JsBool = JsBool.false
rtl: JsBool = JsBool.false
navigation_mode: NavigationMode = NavigationMode.default
shuffle: JsBool = JsBool.false
fragments: JsBool = JsBool.true
fragment_in_url: JsBool = JsBool.true
embedded: JsBool = JsBool.false
help: JsBool = JsBool.true
pause: JsBool = JsBool.true
show_notes: JsBool = JsBool.false
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
preload_iframes: PreloadIframes = PreloadIframes.null
auto_animate: JsBool = JsBool.true
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
auto_animate_duration: float = 1.0
auto_animate_unmatched: JsBool = JsBool.true
auto_animate_styles: List[str] = [
"opacity",
"color",
"background-color",
"padding",
"font-size",
"line-height",
"letter-spacing",
"border-width",
"border-color",
"border-radius",
"outline",
"outline-offset",
]
auto_slide: AutoSlide = 0
auto_slide_stoppable: JsBool = JsBool.true
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
default_timing: Union[JsNull, int] = JsNull.null
mouse_wheel: JsBool = JsBool.false
preview_links: JsBool = JsBool.false
post_message: JsBool = JsBool.true
post_message_events: JsBool = JsBool.false
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_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
pdf_page_height_offset: int = -1
view_distance: int = 3
mobile_view_distance: int = 2
display: Display = Display.block
hide_inactive_cursor: JsBool = JsBool.true
hide_cursor_time: int = 5000
# Add. options
background_color: str = "black" # TODO: use pydantic.color.Color
reveal_version: str = "4.4.0"
reveal_theme: RevealTheme = RevealTheme.black
shuffle: JSBool = JSBool.false
title: str = "Manim Slides"
width: str = "100%"
model_config = ConfigDict(use_enum_values=True, extra="forbid")
class Config:
use_enum_values = True
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))
if slide_config.is_loop():
yield f'<section data-background-video="{file}" data-background-video-loop></section>'
logger.debug(f"Writing video section with file {file}")
if self.data_uri:
file = data_uri(file)
else:
yield f'<section data-background-video="{file}"></section>'
file = assets_dir / file.name
# TODO: document this
# Videos are muted because, otherwise, the first slide never plays correctly.
# This is due to a restriction in playing audio without the user doing anything.
# Later, this might be useful to only mute the first video, or to make it optional.
# 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-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-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."""
return pkg_resources.resource_string(
__name__, "data/revealjs_template.html"
).decode()
if isinstance(self.template, Path):
return self.template.read_text()
def convert_to(self, dest: str) -> None:
if sys.version_info < (3, 9):
return resources.read_text(data, "revealjs_template.html")
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))
if self.data_uri:
assets_dir = Path("") # Actually we won't care.
self.assets_dir = self.assets_dir.format(
dirname=dirname, basename=basename, ext=ext
)
full_assets_dir = os.path.join(dirname, self.assets_dir)
for presentation_config in self.presentation_configs:
presentation_config.concat_animations()
else:
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
os.makedirs(full_assets_dir, exist_ok=True)
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
for presentation_config in self.presentation_configs:
presentation_config.concat_animations().move_to(full_assets_dir)
logger.debug(f"Assets will be saved to: {full_assets_dir}")
full_assets_dir.mkdir(parents=True, exist_ok=True)
for presentation_config in self.presentation_configs:
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())
if self.data_uri:
data_uri_fix = DATA_URI_FIX
else:
data_uri_fix = ""
content = revealjs_template.format(
sections=sections, data_uri_fix=data_uri_fix, **self.dict()
)
f.write(content)
class FrameIndex(str, Enum):
first = "first"
last = "last"
class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0
model_config = ConfigDict(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 PDF presentation, saved to DEST."""
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
cap = cv2.VideoCapture(str(file))
if frame_index == FrameIndex.last:
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return Image.fromarray(frame)
else:
raise ValueError("Failed to read {image_index} image from video file")
images = []
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]
images.append(read_image_from_video_file(file, self.frame_index))
images[0].save(
dest,
"PDF",
resolution=self.resolution,
save_all=True,
append_images=images[1:],
)
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
model_config = ConfigDict(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]
mime_type = mimetypes.guess_type(file)[0]
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=mime_type,
)
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
to = ctx.params.get("to")
to = ctx.params.get("to", "html")
if to:
converter = Converter.from_string(to)(scenes=[])
for key, value in converter.dict().items():
click.echo(f"{key}: {repr(value)}")
converter = Converter.from_string(to)(presentation_configs=[])
for key, value in converter.dict().items():
click.echo(f"{key}: {repr(value)}")
ctx.exit()
ctx.exit()
else:
raise click.UsageError(
"Using --show-config option requires to first specify --to option."
)
return click.option(
return click.option( # type: ignore
"--show-config",
is_flag=True,
help="Show supported options for given format and exit.",
@ -163,13 +605,41 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
)(function)
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wraps a function to add a `--show-template` option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
template = ctx.params.get("template", None)
converter = Converter.from_string(to)(
presentation_configs=[], template=template
)
click.echo(converter.load_template())
ctx.exit()
return click.option( # type: ignore
"--show-template",
is_flag=True,
help="Show the template (currently) used for a given conversion format and exit.",
default=None,
expose_value=False,
show_envvar=True,
callback=callback,
)(function)
@click.command()
@click.argument("scenes", nargs=-1)
@folder_path_option
@click.argument("dest")
@click.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", "pdf", "pptx"], case_sensitive=False),
default="html",
show_default=True,
help="Set the conversion format to use.",
@ -187,18 +657,27 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"config_options",
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. E.g., pass `-cbackground_color=red` to set the background color to red (if supported).",
help="Configuration options passed to the converter. E.g., pass `-cslide_number=true` to display slide numbers.",
)
@click.option(
"--use-template",
"template",
metavar="FILE",
type=click.Path(exists=True, dir_okay=False, 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
@show_config_options
@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[Path],
) -> None:
"""
Convert SCENE(s) into a given format and writes the result in DEST.
@ -206,11 +685,28 @@ def convert(
presentation_configs = get_scenes_presentation_config(scenes, folder)
converter = Converter.from_string(to)(
presentation_configs=presentation_configs, **config_options
)
try:
converter = Converter.from_string(to)(
presentation_configs=presentation_configs,
template=template,
**config_options,
)
converter.convert_to(dest)
converter.convert_to(dest)
if open_result:
converter.open(dest)
if open_result:
converter.open(dest)
except ValidationError as e:
errors = e.errors()
msg = [
f"{len(errors)} error(s) occured with configuration options for '{to}', see below."
]
for error in errors:
option = error["loc"][0]
_msg = error["msg"]
msg.append(f"Option '{option}': {_msg}")
raise click.UsageError("\n".join(msg))

View File

View File

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

View File

@ -1,2 +1,5 @@
FOLDER_PATH: str = "./slides"
CONFIG_PATH: str = ".manim-slides.json"
from pathlib import Path
FOLDER_PATH: Path = Path("./slides")
CONFIG_PATH: Path = Path(".manim-slides.toml")
FFMPEG_BIN: Path = Path("ffmpeg")

View File

View File

@ -0,0 +1,426 @@
# type: ignore
r"""
A directive for including Manim slides in a Sphinx document
===========================================================
.. warning::
This Sphinx extension requires Manim to be installed,
and won't probably work on ManimGL examples.
.. note::
The current implementation is highly inspired from Manim's own
sphinx directive, from v0.17.3.
When rendering the HTML documentation, the ``.. manim-slides::``
directive implemented here allows to include rendered videos.
This directive requires three additional dependencies:
``manim``, ``docutils`` and ``jinja2``. The last two are usually bundled
with Sphinx.
You can install them manually, or with the extra keyword:
pip install manim-slides[sphinx-directive]
Note that you will still need to install Manim's platform-specific dependencies,
see
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
Usage
-----
First, you must include the directive in the Sphinx configuration file:
.. code-block:: python
:caption: Sphinx configuration file (usually :code:`docs/source/conf.py`).
:emphasize-lines: 3
extensions = [
# ...
"manim_slides.docs.manim_slides_directive",
]
Its basic usage that allows processing **inline content**
looks as follows::
.. manim-slides:: MySlide
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
...
It is required to pass the name of the class representing the
scene to be rendered to the directive.
As a second application, the directive can also be used to
render scenes that are defined within doctests, for example::
.. manim-slides:: DirectiveDoctestExample
:ref_classes: Dot
>>> from manim import Create, Dot, RED
>>> from manim_slides import Slide
>>> dot = Dot(color=RED)
>>> dot.color
<Color #fc6255>
>>> class DirectiveDoctestExample(Slide):
... def construct(self):
... self.play(Create(dot))
Options
-------
Options can be passed as follows::
.. manim-slides:: <Class name>
:<option name>: <value>
The following configuration options are supported by the
directive:
hide_source
If this flag is present without argument,
the source code is not displayed above the rendered video.
quality : {'low', 'medium', 'high', 'fourk'}
Controls render quality of the video, in analogy to
the corresponding command line flags.
ref_classes
A list of classes, separated by spaces, that is
rendered in a reference block after the source code.
ref_functions
A list of functions, separated by spaces,
that is rendered in a reference block after the source code.
ref_methods
A list of methods, separated by spaces,
that is rendered in a reference block after the source code.
"""
from __future__ import annotations
import csv
import itertools as it
import re
import sys
from pathlib import Path
from timeit import timeit
import jinja2
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import StringList
from manim import QUALITIES
from ..convert import RevealJS
from ..present import get_scenes_presentation_config
classnamedict = {}
class SkipManimNode(nodes.Admonition, nodes.Element):
"""Auxiliary node class that is used when the ``skip-manim-slides`` tag is
present or ``.pot`` files are being built.
Skips rendering the manim-slides directive and outputs a placeholder instead.
"""
pass
def visit(self, node, name=""):
self.visit_admonition(node, name)
if not isinstance(node[0], nodes.title):
node.insert(0, nodes.title("skip-manim-slides", "Example Placeholder"))
def depart(self, node):
self.depart_admonition(node)
def process_name_list(option_input: str, reference_type: str) -> list[str]:
r"""Reformats a string of space separated class names
as a list of strings containing valid Sphinx references.
Tests
-----
::
>>> process_name_list("Tex TexTemplate", "class")
[':class:`~.Tex`', ':class:`~.TexTemplate`']
>>> process_name_list("Scene.play Mobject.rotate", "func")
[':func:`~.Scene.play`', ':func:`~.Mobject.rotate`']
"""
return [f":{reference_type}:`~.{name}`" for name in option_input.split()]
class ManimSlidesDirective(Directive):
r"""The manim-slides directive, rendering videos while building
the documentation.
See the module docstring for documentation.
"""
has_content = True
required_arguments = 1
optional_arguments = 0
option_spec = {
"hide_source": bool,
"quality": lambda arg: directives.choice(
arg,
("low", "medium", "high", "fourk"),
),
"ref_modules": lambda arg: process_name_list(arg, "mod"),
"ref_classes": lambda arg: process_name_list(arg, "class"),
"ref_functions": lambda arg: process_name_list(arg, "func"),
"ref_methods": lambda arg: process_name_list(arg, "meth"),
}
final_argument_whitespace = True
def run(self):
# Rendering is skipped if the tag skip-manim is present,
# or if we are making the pot-files
should_skip = (
"skip-manim-slides"
in self.state.document.settings.env.app.builder.tags.tags
or self.state.document.settings.env.app.builder.name == "gettext"
)
if should_skip:
node = SkipManimNode()
self.state.nested_parse(
StringList(
[
f"Placeholder block for ``{self.arguments[0]}``.",
"",
".. code-block:: python",
"",
]
+ [" " + line for line in self.content]
),
self.content_offset,
node,
)
return [node]
from manim import config, tempconfig
global classnamedict
clsname = self.arguments[0]
if clsname not in classnamedict:
classnamedict[clsname] = 1
else:
classnamedict[clsname] += 1
hide_source = "hide_source" in self.options
ref_content = (
self.options.get("ref_modules", [])
+ self.options.get("ref_classes", [])
+ self.options.get("ref_functions", [])
+ self.options.get("ref_methods", [])
)
if ref_content:
ref_block = "References: " + " ".join(ref_content)
else:
ref_block = ""
if "quality" in self.options:
quality = f'{self.options["quality"]}_quality'
else:
quality = "example_quality"
frame_rate = QUALITIES[quality]["frame_rate"]
pixel_height = QUALITIES[quality]["pixel_height"]
pixel_width = QUALITIES[quality]["pixel_width"]
state_machine = self.state_machine
document = state_machine.document
source_file_name = Path(document.attributes["source"])
source_rel_name = source_file_name.relative_to(setup.confdir)
source_rel_dir = source_rel_name.parents[0]
dest_dir = Path(setup.app.builder.outdir, source_rel_dir).absolute()
if not dest_dir.exists():
dest_dir.mkdir(parents=True, exist_ok=True)
source_block = [
".. code-block:: python",
"",
*(" " + line for line in self.content),
]
source_block = "\n".join(source_block)
config.media_dir = (Path(setup.confdir) / "media").absolute()
config.images_dir = "{media_dir}/images"
config.video_dir = "{media_dir}/videos/{quality}"
output_file = f"{clsname}-{classnamedict[clsname]}"
config.assets_dir = Path("_static")
config.progress_bar = "none"
config.verbosity = "WARNING"
example_config = {
"frame_rate": frame_rate,
"pixel_height": pixel_height,
"pixel_width": pixel_width,
"output_file": output_file,
}
user_code = self.content
if user_code[0].startswith(">>> "): # check whether block comes from doctest
user_code = [
line[4:] for line in user_code if line.startswith((">>> ", "... "))
]
code = [
"from manim import *",
*user_code,
f"{clsname}().render()",
]
try:
with tempconfig(example_config):
run_time = timeit(lambda: exec("\n".join(code), globals()), number=1)
video_dir = config.get_dir("video_dir")
except Exception as e:
raise RuntimeError(f"Error while rendering example {clsname}") from e
_write_rendering_stats(
clsname,
run_time,
self.state.document.settings.env.docname,
)
# copy video file to output directory
filename = f"{output_file}.html"
filesrc = video_dir / filename
destfile = Path(dest_dir, filename)
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")
)
RevealJS(presentation_configs=presentation_configs, controls="true").convert_to(
destfile
)
# shutil.copyfile(filesrc, destfile)
print("CLASS NAME:", clsname)
rendered_template = jinja2.Template(TEMPLATE).render(
clsname=clsname,
clsname_lowercase=clsname.lower(),
hide_source=hide_source,
filesrc_rel=Path(filesrc).relative_to(setup.confdir).as_posix(),
output_file=output_file,
source_block=source_block,
ref_block=ref_block,
)
state_machine.insert_input(
rendered_template.split("\n"),
source=document.attributes["source"],
)
return []
rendering_times_file_path = Path("../rendering_times.csv")
def _write_rendering_stats(scene_name, run_time, file_name):
with rendering_times_file_path.open("a") as file:
csv.writer(file).writerow(
[
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
scene_name,
"%.3f" % run_time,
],
)
def _log_rendering_times(*args):
if rendering_times_file_path.exists():
with rendering_times_file_path.open() as file:
data = list(csv.reader(file))
if len(data) == 0:
sys.exit()
print("\nRendering Summary\n-----------------\n")
max_file_length = max(len(row[0]) for row in data)
for key, group in it.groupby(data, key=lambda row: row[0]):
key = key.ljust(max_file_length + 1, ".")
group = list(group)
if len(group) == 1:
row = group[0]
print(f"{key}{row[2].rjust(7, '.')}s {row[1]}")
continue
time_sum = sum(float(row[2]) for row in group)
print(
f"{key}{f'{time_sum:.3f}'.rjust(7, '.')}s => {len(group)} EXAMPLES",
)
for row in group:
print(f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}")
print("")
def _delete_rendering_times(*args):
if rendering_times_file_path.exists():
rendering_times_file_path.unlink()
def setup(app):
app.add_node(SkipManimNode, html=(visit, depart))
setup.app = app
setup.config = app.config
setup.confdir = app.confdir
app.add_directive("manim-slides", ManimSlidesDirective)
app.connect("builder-inited", _delete_rendering_times)
app.connect("build-finished", _log_rendering_times)
metadata = {"parallel_read_safe": False, "parallel_write_safe": True}
return metadata
TEMPLATE = r"""
{% if not hide_source %}
.. raw:: html
<div id="{{ clsname_lowercase }}" class="admonition admonition-manim-example">
<p class="admonition-title">Example: {{ clsname }} <a class="headerlink" href="#{{ clsname_lowercase }}">¶</a></p>
{% endif %}
.. raw:: html
<div style="position:relative;padding-bottom:56.25%;">
<iframe
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
frameborder="0"
width="100%"
height="100%"
allowfullscreen
allow="autoplay"
src="./{{ output_file }}.html">
</iframe>
</div>
{% if not hide_source %}
{{ source_block }}
{{ ref_block }}
.. raw:: html
</div>
{% endif %}
"""

View File

@ -0,0 +1,265 @@
"""
Utilities for using Manim Slides with IPython (in particular: Jupyter notebooks).
=================================================================================
.. toctree::
:hidden:
magic_example
.. note::
The current implementation is highly inspired from Manim's own
IPython magics, from v0.17.3.
This magic requires two additional dependencies: ``manim`` and ``IPython``.
You can install them manually, or with the extra keyword:
pip install manim-slides[magic]
Note that you will still need to install Manim's platform-specific dependencies,
see
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
"""
from __future__ import annotations
import logging
import mimetypes
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
from IPython import get_ipython
from IPython.core.interactiveshell import InteractiveShell
from IPython.core.magic import Magics, line_cell_magic, magics_class, needs_local_scope
from IPython.display import HTML, display
from manim import config, logger, tempconfig
from manim.__main__ import main
from manim.constants import RendererType
from manim.renderer.shader import shader_program_cache
from ..convert import RevealJS
from ..present import get_scenes_presentation_config
@magics_class
class ManimSlidesMagic(Magics): # type: ignore
def __init__(self, shell: InteractiveShell) -> None:
super().__init__(shell)
self.rendered_files: Dict[Path, Path] = {}
@needs_local_scope
@line_cell_magic
def manim_slides(
self,
line: str,
cell: Optional[str] = None,
local_ns: Dict[str, Any] = {},
) -> None:
r"""Render Manim Slides contained in IPython cells.
Works as a line or cell magic.
.. note::
This magic works pretty much like the one from Manim, except that it
will render Manim Slides using RevealJS. For passing arguments to
Manim Slides' convert module, use ``-manim-slides key=value``.
Everything that is after ``--manim-slides`` will be send to
Manim Slides' command. E.g., use ``--manim-slides controls=true``
to display control buttons.
.. hint::
This line and cell magic works best when used in a JupyterLab
environment: while all of the functionality is available for
classic Jupyter notebooks as well, it is possible that videos
sometimes don't update on repeated execution of the same cell
if the scene name stays the same.
This problem does not occur when using JupyterLab.
Please refer to `<https://jupyter.org/>`_ for more information about JupyterLab
and Jupyter notebooks.
Usage in line mode::
%manim_slides [CLI options] MyAwesomeSlide
Usage in cell mode::
%%manim_slides [CLI options] MyAwesomeSlide
class MyAweseomeSlide(Slide):
def construct(self):
...
Run ``%manim_slides --help`` and ``%manim_slides render --help``
for possible command line interface options.
.. note::
The maximal width of the rendered videos that are displayed in the notebook can be
configured via the ``media_width`` configuration option. The default is set to ``25vw``,
which is 25% of your current viewport width. To allow the output to become as large
as possible, set ``config.media_width = "100%"``.
The ``media_embed`` option will embed the image/video output in the notebook. This is
generally undesirable as it makes the notebooks very large, but is required on some
platforms (notably Google's CoLab, where it is automatically enabled unless suppressed
by ``config.embed = False``) and needed in cases when the notebook (or converted HTML
file) will be moved relative to the video locations. Use-cases include building
documentation with Sphinx and JupyterBook. See also the
:mod:`Manim Slides directive for Sphinx
<manim_slides.docs.manim_slides_directive>`.
Examples
--------
First make sure to put ``from manim_slides import ManimSlidesMagic``,
or even ``from manim_slides import *``
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
could look as follows::
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
class MySlide(Slide):
def construct(self):
square = Square()
circle = Circle()
self.play(Create(square))
self.next_slide()
self.play(Transform(square, circle))
Evaluating this cell will render and display the ``MySlide`` slide
defined in the body of the cell.
.. note::
In case you want to hide the red box containing the output progress bar, the ``progress_bar`` config
option should be set to ``None``. This can also be done by passing ``--progress_bar None`` as a
CLI flag.
"""
if cell:
exec(cell, local_ns)
split_args = line.split("--manim-slides", 2)
manim_args = split_args[0].split()
if len(split_args) == 2:
manim_slides_args = split_args[1].split()
else:
manim_slides_args = []
args = manim_args
if not len(args) or "-h" in args or "--help" in args or "--version" in args:
main(args, standalone_mode=False, prog_name="manim")
return
modified_args = self.add_additional_args(args)
args = main(modified_args, standalone_mode=False, prog_name="manim")
with tempconfig(local_ns.get("config", {})):
config.digest_args(args)
logging.getLogger("manim-slides").setLevel(logging.getLogger("manim").level)
renderer = None
if config.renderer == RendererType.OPENGL:
from manim.renderer.opengl_renderer import OpenGLRenderer
renderer = OpenGLRenderer()
try:
SceneClass = local_ns[config["scene_names"][0]]
scene = SceneClass(renderer=renderer)
scene.render()
finally:
# Shader cache becomes invalid as the context is destroyed
shader_program_cache.clear()
# Close OpenGL window here instead of waiting for the main thread to
# finish causing the window to stay open and freeze
if renderer is not None and renderer.window is not None:
renderer.window.close()
if config["output_file"] is None:
logger.info("No output file produced")
return
local_path = Path(config["output_file"]).relative_to(Path.cwd())
tmpfile = (
Path(config["media_dir"]) / "jupyter" / f"{_generate_file_name()}.html"
)
if local_path in self.rendered_files:
self.rendered_files[local_path].unlink()
pass
self.rendered_files[local_path] = tmpfile
tmpfile.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(local_path, tmpfile)
file_type = mimetypes.guess_type(config["output_file"])[0] or "video/mp4"
embed = config["media_embed"]
if embed is None:
# videos need to be embedded when running in google colab.
# do this automatically in case config.media_embed has not been
# set explicitly.
embed = "google.colab" in str(get_ipython())
if not file_type.startswith("video"):
raise ValueError(
f"Manim Slides only supports video files, not {file_type}"
)
clsname = config["scene_names"][0]
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
if embed: # Embedding implies data-uri
kwargs["data_uri"] = "true"
# TODO: FIXME
# Seems like files are blocked so date-uri is the only working option...
if kwargs.get("data_uri", "false").lower().strip() == "false":
logger.warn(
"data_uri option is currently automatically enabled, "
"because using local video files does not seem to work properly."
)
kwargs["data_uri"] = "true"
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")
)
RevealJS(presentation_configs=presentation_configs, **kwargs).convert_to(
tmpfile
)
if embed:
result = HTML(
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" srcdoc="{srcdoc}"></iframe></div>""".format(
srcdoc=tmpfile.read_text().replace('"', "'")
)
)
else:
result = HTML(
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{src}"></iframe></div>""".format(
src=tmpfile.as_posix()
)
)
display(result)
def add_additional_args(self, args: list[str]) -> list[str]:
additional_args = ["--jupyter"]
# Use webm to support transparency
if "-t" in args and "--format" not in args:
additional_args += ["--format", "webm"]
return additional_args + args[:-1] + [""] + [args[-1]]
def _generate_file_name() -> str:
return config["scene_names"][0] + "@" + datetime.now().strftime("%Y-%m-%d@%H-%M-%S") # type: ignore

49
manim_slides/logger.py Normal file
View File

@ -0,0 +1,49 @@
"""
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",
"Pressed key",
]
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.setLevel(logging.getLogger("manim").level)
logger.addHandler(rich_handler)
return logger
logger = logging.getLogger("manim-slides")

View File

@ -1,10 +1,10 @@
import os
import sys
from contextlib import contextmanager
from importlib.util import find_spec
from typing import Iterator
__all__ = [
# Constants
"FFMPEG_BIN",
"LEFT",
"MANIM",
"MANIM_PACKAGE_NAME",
"MANIM_AVAILABLE",
@ -13,25 +13,19 @@ __all__ = [
"MANIMGL_PACKAGE_NAME",
"MANIMGL_AVAILABLE",
"MANIMGL_IMPORTED",
"logger",
# Classes
"AnimationGroup",
"FadeIn",
"FadeOut",
"Mobject",
"Scene",
"ThreeDScene",
# Objects
"logger",
"config",
"FFMPEG_BIN",
]
@contextmanager
def suppress_stdout() -> Iterator[None]:
with open(os.devnull, "w") as devnull:
old_stdout = sys.stdout
sys.stdout = devnull
try:
yield
finally:
sys.stdout = old_stdout
MANIM_PACKAGE_NAME = "manim"
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
@ -43,8 +37,8 @@ MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
if MANIM_IMPORTED and MANIMGL_IMPORTED:
from manim import logger
logger.warn(
"Both manim and manimgl are installed, therefore `manim-slide` needs to need which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
logger.warning(
"Both manim and manimgl are imported, therefore `manim-slide` needs to know which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
)
MANIM = True
MANIMGL = False
@ -67,15 +61,33 @@ else:
if MANIMGL:
from manimlib import Scene, ThreeDScene, config
from manimlib import (
LEFT,
AnimationGroup,
FadeIn,
FadeOut,
Mobject,
Scene,
ThreeDScene,
config,
)
from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log as logger
else:
with suppress_stdout(): # Avoids printing "Manim Community v..."
from manim import Scene, ThreeDScene, config, logger
from manim import (
LEFT,
AnimationGroup,
FadeIn,
FadeOut,
Mobject,
Scene,
ThreeDScene,
config,
logger,
)
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable

View File

@ -1,23 +1,35 @@
import os
import platform
import signal
import sys
import time
from enum import IntEnum, auto, unique
from enum import Enum, IntFlag, 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_extra_types.color import Color
from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent
from PySide6.QtGui import (
QCloseEvent,
QIcon,
QImage,
QKeyEvent,
QPixmap,
QResizeEvent,
QScreen,
)
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
from tqdm import tqdm
from .commons import config_path_option, verbosity_option
from .config import DEFAULT_CONFIG, Config, PresentationConfig, SlideConfig
from .defaults import FOLDER_PATH
from .manim import logger
from .logger import logger
from .resources import * # noqa: F401, F403
os.environ.pop(
@ -28,9 +40,17 @@ WINDOW_NAME = "Manim Slides"
WINDOW_INFO_NAME = f"{WINDOW_NAME}: Info"
WINDOWS = platform.system() == "Windows"
class AspectRatio(Enum):
ignore = Qt.IgnoreAspectRatio
keep = Qt.KeepAspectRatio
auto = "auto"
ASPECT_RATIO_MODES = {
"ignore": Qt.IgnoreAspectRatio,
"keep": Qt.KeepAspectRatio,
"ignore": AspectRatio.ignore,
"keep": AspectRatio.keep,
"auto": AspectRatio.auto,
}
RESIZE_MODES = {
@ -40,16 +60,20 @@ RESIZE_MODES = {
@unique
class State(IntEnum):
class State(IntFlag):
"""Represents all possible states of a slide presentation."""
"""A video is actively being played."""
PLAYING = auto()
"""A video was manually paused."""
PAUSED = auto()
"""Waiting for user to press next (or else)."""
WAIT = auto()
"""Presentation was terminated."""
END = auto()
def __str__(self) -> str:
return self.name.capitalize()
return self.name.capitalize() # type: ignore
def now() -> float:
@ -61,12 +85,11 @@ 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 = ""
self.current_file: Path = Path("")
self.loaded_animation_cap: int = -1
self.cap = None # cap = cv2.VideoCapture
@ -78,6 +101,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."""
@ -106,20 +194,19 @@ 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: Path = self.files[animation]
if self.reverse:
file = "{}_reversed{}".format(*os.path.splitext(file))
file = file.parent / f"{file.stem}_reversed{file.suffix}"
self.reversed_animation = animation
self.current_file = file
self.cap = cv2.VideoCapture(file)
self.cap = cv2.VideoCapture(str(file))
self.loaded_animation_cap = animation
@property
@ -130,7 +217,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:
@ -168,9 +257,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
@ -181,7 +271,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."""
@ -192,10 +283,10 @@ class Presentation:
def load_last_slide(self) -> None:
"""Loads last slide."""
self.current_slide_index = len(self.slides) - 2
self.current_slide_index = len(self.slides) - 1
assert (
self.current_slide_index >= 0
), "Slides should be at list of a least two elements"
), "Slides should be at list of a least one element"
self.current_animation = self.current_slide.start_animation
self.load_animation_cap(self.current_animation)
self.slides[-1].terminated = False
@ -209,7 +300,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
@ -228,41 +319,37 @@ class Presentation:
It does this by reading the video information and checking if the state is still correct.
It returns the frame to show (lastframe) and the new state.
"""
if state == State.PAUSED:
if state ^ State.PLAYING: # If not playing, we return the same
if self.lastframe is None:
_, self.lastframe = self.current_cap.read()
return self.lastframe, state
still_playing, frame = self.current_cap.read()
if still_playing:
self.lastframe = frame
elif state == state.WAIT or state == state.PAUSED: # type: ignore
return self.lastframe, state
elif self.current_slide.is_last() and self.current_slide.terminated:
return self.lastframe, State.END
else: # not still playing
if self.is_last_animation:
if self.current_slide.is_slide():
return self.lastframe, State.PLAYING
# Video was terminated
if self.is_last_animation:
if self.current_slide.is_loop():
if self.reverse:
state = State.WAIT
elif self.current_slide.is_loop():
if self.reverse:
state = State.WAIT
else:
self.current_animation = self.current_slide.start_animation
state = State.PLAYING
self.rewind_current_slide()
elif self.current_slide.is_last():
self.current_slide.terminated = True
elif (
self.current_slide.is_last()
and self.current_slide.end_animation == self.current_animation
):
state = State.WAIT
else:
self.current_animation = self.current_slide.start_animation
state = State.PLAYING
self.rewind_current_slide()
elif self.current_slide.is_last():
state = State.END
else:
# Play next video!
self.current_animation = self.next_animation
self.load_animation_cap(self.current_animation)
# Reset video to position zero if it has been played before
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
state = State.WAIT
else:
# Play next video!
self.current_animation = self.next_animation
self.load_animation_cap(self.current_animation)
# Reset video to position zero if it has been played before
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
return self.lastframe, state
@ -272,16 +359,20 @@ class Display(QThread): # type: ignore
change_video_signal = Signal(np.ndarray)
change_info_signal = Signal(dict)
change_presentation_signal = 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
@ -289,21 +380,66 @@ class Display(QThread): # type: ignore
self.config = config
self.skip_all = skip_all
self.record_to = record_to
self.recordings: List[Tuple[str, int, int]] = []
self.recordings: List[Tuple[Path, int, int]] = []
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_signal.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
@property
def is_last_presentation(self) -> bool:
"""Returns True if current presentation is the last one."""
return self.current_presentation_index == len(self) - 1
def start(self) -> None:
super().start()
self.change_presentation_signal.emit()
def run(self) -> None:
"""Runs a series of presentations until end or exit."""
while self.run_flag:
@ -311,18 +447,15 @@ class Display(QThread): # type: ignore
self.lastframe, self.state = self.current_presentation.update_state(
self.state
)
if self.state == State.PLAYING or self.state == State.PAUSED:
if self.state & (State.PLAYING | State.PAUSED):
if self.start_paused:
self.state = State.PAUSED
self.start_paused = False
if self.state == State.END:
if self.state & State.END:
if self.current_presentation_index == len(self.presentations) - 1:
if self.exit_after_last_slide:
self.run_flag = False
continue
else:
self.current_presentation_index += 1
self.state = State.PLAYING
self.handle_key()
self.show_video()
@ -330,6 +463,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()
@ -338,7 +486,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:
@ -347,7 +495,7 @@ class Display(QThread): # type: ignore
)
file, frame_number, fps = self.recordings[0]
cap = cv2.VideoCapture(file)
cap = cv2.VideoCapture(str(file))
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
_, frame = cap.read()
@ -363,7 +511,7 @@ class Display(QThread): # type: ignore
if file != _file:
cap.release()
file = _file
cap = cv2.VideoCapture(_file)
cap = cv2.VideoCapture(str(_file))
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
_, frame = cap.read()
@ -406,37 +554,43 @@ class Display(QThread): # type: ignore
"""Handles key strokes."""
key = self.key
keys = self.config.keys
if self.config.QUIT.match(key):
if keys.QUIT.match(key):
self.run_flag = False
elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key):
elif self.state == State.PLAYING and keys.PLAY_PAUSE.match(key):
self.state = State.PAUSED
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
elif self.state == State.PAUSED and keys.PLAY_PAUSE.match(key):
self.state = State.PLAYING
elif self.state == State.WAIT and (
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
elif self.state & (State.END | State.WAIT) and (
keys.CONTINUE.match(key) or keys.PLAY_PAUSE.match(key) or self.skip_all
):
self.current_presentation.load_next_slide()
if (self.state & State.END) and not self.is_last_presentation:
self.current_presentation_index += 1
self.current_presentation.rewind_current_slide()
else:
self.current_presentation.load_next_slide()
self.state = State.PLAYING
elif (
self.state == State.PLAYING and self.config.CONTINUE.match(key)
self.state == State.PLAYING and keys.CONTINUE.match(key)
) or self.skip_all:
self.current_presentation.load_next_slide()
elif self.config.BACK.match(key):
elif keys.BACK.match(key):
if self.current_presentation.current_slide_index == 0:
if self.current_presentation_index == 0:
self.current_presentation.load_previous_slide()
else:
self.current_presentation.cancel_reverse()
self.current_presentation_index -= 1
self.current_presentation.load_last_slide()
self.state = State.PLAYING
else:
self.current_presentation.load_previous_slide()
self.state = State.PLAYING
elif self.config.REVERSE.match(key):
elif keys.REVERSE.match(key):
self.current_presentation.reverse_current_slide()
self.state = State.PLAYING
elif self.config.REWIND.match(key):
elif keys.REWIND.match(key):
self.current_presentation.cancel_reverse()
self.current_presentation.rewind_current_slide()
self.state = State.PLAYING
@ -512,19 +666,28 @@ 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: Qt.AspectRatioMode = Qt.IgnoreAspectRatio,
aspect_ratio: AspectRatio = AspectRatio.auto,
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
background_color: str = "black",
screen: Optional[QScreen] = None,
**kwargs: Any,
):
super().__init__()
if screen:
self.setScreen(screen)
self.move(screen.geometry().topLeft())
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
@ -533,17 +696,15 @@ class App(QWidget): # type: ignore
self.setCursor(Qt.BlankCursor)
self.label = QLabel(self)
if self.aspect_ratio == AspectRatio.auto:
self.label.setScaledContents(True)
self.label.setAlignment(Qt.AlignCenter)
self.label.resize(self.display_width, self.display_height)
self.label.setStyleSheet(f"background-color: {background_color}")
self.pixmap = QPixmap(self.width(), self.height())
self.label.setPixmap(self.pixmap)
self.label.setMinimumSize(1, 1)
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
# create the info dialog
self.info = Info()
self.info.show()
@ -553,10 +714,13 @@ class App(QWidget): # type: ignore
if fullscreen:
self.showFullScreen()
else:
self.resize(self.display_width, self.display_height)
# 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_signal.connect(self.update_canvas)
self.thread.finished.connect(self.closeAll)
self.send_key_signal.connect(self.thread.set_key)
@ -564,9 +728,8 @@ 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.config.keys.HIDE_MOUSE.match(key):
if self.hide_mouse:
self.setCursor(Qt.ArrowCursor)
self.hide_mouse = False
@ -584,10 +747,11 @@ class App(QWidget): # type: ignore
self.deleteLater()
def resizeEvent(self, event: QResizeEvent) -> None:
self.pixmap = self.pixmap.scaled(
self.width(), self.height(), self.aspect_ratio, self.resize_mode
)
self.label.setPixmap(self.pixmap)
if not self.label.hasScaledContents():
self.pixmap = self.pixmap.scaled(
self.width(), self.height(), self.aspect_ratio.value, self.resize_mode
)
self.label.setPixmap(self.pixmap)
self.label.resize(self.width(), self.height())
def closeEvent(self, event: QCloseEvent) -> None:
@ -601,54 +765,71 @@ class App(QWidget): # type: ignore
bytes_per_line = ch * w
qt_img = QImage(cv_img.data, w, h, bytes_per_line, QImage.Format_BGR888)
if w != self.width() or h != self.height():
if not self.label.hasScaledContents() and (
w != self.width() or h != self.height()
):
qt_img = qt_img.scaled(
self.width(), self.height(), self.aspect_ratio, self.resize_mode
self.width(), self.height(), self.aspect_ratio.value, self.resize_mode
)
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")
w, h = self.thread.current_resolution
if not self.isFullScreen() and (
self.display_width != w or self.display_height != h
):
self.display_width, self.display_height = w, h
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.from_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))
@ -677,13 +858,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."""
@ -692,19 +873,50 @@ 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"
)
try:
presentation_configs.append(PresentationConfig.parse_file(config_file))
presentation_configs.append(PresentationConfig.from_file(config_file))
except ValidationError as e:
raise click.UsageError(str(e))
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
@ -712,7 +924,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,
)
@ -722,23 +934,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.",
)
@ -755,8 +966,8 @@ def get_scenes_presentation_config(
@click.option(
"--aspect-ratio",
type=click.Choice(ASPECT_RATIO_MODES.keys(), case_sensitive=False),
default="ignore",
help="Set the aspect ratio mode to be used when rescaling video.",
default="auto",
help="Set the aspect ratio mode to be used when rescaling video. `'auto'` option is equivalent to `'ignore'`, but can be much faster due to not calling `scaled()` method on every frame.",
show_default=True,
)
@click.option(
@ -772,26 +983,76 @@ 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.option(
"--screen",
"screen_number",
metavar="NUMBER",
type=int,
default=None,
help="Presents content on the given screen (a.k.a. display).",
)
@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],
screen_number: Optional[int] = None,
) -> None:
"""
Present SCENE(s), one at a time, in order.
@ -806,14 +1067,33 @@ 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):
# TODO: remove me in v5
if config_path.suffix == ".json" or Path(".manim-slides.json").exists():
logger.warn(
"Manim Slides now uses a TOML file for configuration. "
"Please create a new configuration file with `manim-slides init` "
"and move all the keys from your old config, if needed. "
"Then, delete your old JSON config file."
)
if config_path.exists():
try:
config = Config.parse_file(config_path)
config = Config.from_file(config_path)
except ValidationError as e:
raise click.UsageError(str(e))
else:
@ -821,28 +1101,70 @@ 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."
"Recording only support '.avi' extension. "
"For other video formats, "
"please convert the resulting '.avi' file afterwards."
)
app = QApplication(sys.argv)
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]
if not QApplication.instance():
app = QApplication(sys.argv)
else:
app = QApplication.instance()
app.setApplicationName("Manim Slides")
if screen_number is not None:
try:
screen = app.screens()[screen_number]
except IndexError:
logger.error(
f"Invalid screen number {screen_number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
)
screen = None
else:
screen = None
a = App(
presentations,
config=config,
start_paused=start_paused,
fullscreen=fullscreen,
skip_all=skip_all,
resolution=resolution,
record_to=record_to,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio=ASPECT_RATIO_MODES[aspect_ratio],
resize_mode=RESIZE_MODES[resize_mode],
background_color=background_color,
start_at_scene_number=start_at_scene_number,
start_at_slide_number=start_at_slide_number,
start_at_animation_number=start_at_animation_number,
screen=screen,
)
a.show()
# inform about CTRL+C
def sigkill_handler(signum, frame): # type: ignore
logger.warn(
"Thie application cannot be closed with usual CTRL+C, "
"please use the appropriate key defined in your config "
"(default: q)."
)
raise KeyboardInterrupt
signal.signal(signal.SIGINT, sigkill_handler)
sys.exit(app.exec_())
logger.debug("After")

View File

@ -1,19 +1,42 @@
import os
import platform
import shutil
import subprocess
from typing import Any, List, Optional
from pathlib import Path
from typing import (
Any,
List,
Mapping,
MutableMapping,
Optional,
Sequence,
Tuple,
ValuesView,
)
from warnings import warn
import numpy as np
from tqdm import tqdm
from .config import PresentationConfig, SlideConfig, SlideType
from .defaults import FOLDER_PATH
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
from .manim import (
FFMPEG_BIN,
LEFT,
MANIMGL,
AnimationGroup,
FadeIn,
FadeOut,
Mobject,
Scene,
ThreeDScene,
config,
logger,
)
def reverse_video_file(src: str, dst: str) -> None:
def reverse_video_file(src: Path, dst: Path) -> None:
"""Reverses a video file, writting the result to `dst`."""
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
command = [str(FFMPEG_BIN), "-y", "-i", str(src), "-vf", "reverse", str(dst)]
logger.debug(" ".join(command))
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
@ -27,15 +50,14 @@ def reverse_video_file(src: str, dst: str) -> None:
class Slide(Scene): # type:ignore
"""
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools for slides rendering.
"""
def __init__(
self, *args: Any, output_folder: str = FOLDER_PATH, **kwargs: Any
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
) -> None:
if MANIMGL:
if not os.path.isdir("videos"):
os.mkdir("videos")
Path("videos").mkdir(exist_ok=True)
kwargs["file_writer_config"] = {
"break_into_partial_movies": True,
"output_directory": "",
@ -46,15 +68,49 @@ 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: Path = 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.__canvas: MutableMapping[str, Mobject] = {}
self.__wait_time_between_slides = 0.0
@property
def partial_movie_files(self) -> List[str]:
def __frame_height(self) -> float:
"""Returns the scene's frame height."""
if MANIMGL:
return self.frame_height # type: ignore
else:
return config["frame_height"] # type: ignore
@property
def __frame_width(self) -> float:
"""Returns the scene's frame width."""
if MANIMGL:
return self.frame_width # type: ignore
else:
return config["frame_width"] # type: ignore
@property
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[Path]:
"""Returns a list of partial movie files, a.k.a animations."""
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
@ -63,155 +119,432 @@ class Slide(Scene): # type:ignore
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
files = get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
files = self.renderer.file_writer.partial_movie_files
return [Path(file) for file in files]
@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
@property
def canvas(self) -> MutableMapping[str, Mobject]:
"""
Returns the canvas associated to the current slide.
The canvas is a mapping between names and Mobjects,
for objects that are assumed to stay in multiple slides.
For example, a section title or a slide number.
Examples
--------
.. manim-slides:: CanvasExample
from manim import *
from manim_slides import Slide
class CanvasExample(Slide):
def update_canvas(self):
self.counter += 1
old_slide_number = self.canvas["slide_number"]
new_slide_number = Text(f"{self.counter}").move_to(old_slide_number)
self.play(Transform(old_slide_number, new_slide_number))
def construct(self):
title = Text("My Title").to_corner(UL)
self.counter = 1
slide_number = Text("1").to_corner(DL)
self.add_to_canvas(title=title, slide_number=slide_number)
self.play(FadeIn(title), FadeIn(slide_number))
self.next_slide()
circle = Circle(radius=2)
dot = Dot()
self.update_canvas()
self.play(Create(circle))
self.play(MoveAlongPath(dot, circle))
self.next_slide()
self.update_canvas()
square = Square()
self.play(self.wipe(self.mobjects_without_canvas, square))
self.next_slide()
self.update_canvas()
self.play(
Transform(
self.canvas["title"],
Text("New Title").to_corner(UL)
)
)
self.next_slide()
self.remove_from_canvas("title", "slide_number")
self.play(self.wipe(self.mobjects_without_canvas, []))
"""
return self.__canvas
def add_to_canvas(self, **objects: Mobject) -> Mobject:
"""
Adds objects to the canvas, using key values as names.
:param objects: A mapping between names and Mobjects.
.. note::
This method does not actually do anything in terms of
animations. You must still call :code:`self.add` or
play some animation that introduces each Mobject for
it to appear. The same applies when removing objects.
"""
self.__canvas.update(objects)
def remove_from_canvas(self, *names: str) -> None:
"""
Removes objects from the canvas.
"""
for name in names:
self.__canvas.pop(name)
@property
def canvas_mobjects(self) -> ValuesView[Mobject]:
"""
Returns Mobjects contained in the canvas.
"""
return self.canvas.values()
@property
def mobjects_without_canvas(self) -> Sequence[Mobject]:
"""
Returns the list of objects contained in the scene,
minus those present in the canvas.
"""
return [
mobject for mobject in self.mobjects if mobject not in self.canvas_mobjects
]
@property
def wait_time_between_slides(self) -> float:
r"""
Returns the wait duration (in seconds) added between two slides.
By default, this value is set to 0.
Setting this value to something bigger than 0 will result in a
:code:`self.wait` animation called at the end of every slide.
.. note::
This is useful because animations are usually only terminated
when a new animation is played. You can observe the small difference
in the examples below: the circle is not fully complete in the first
slide of the first example, but well in the second example.
Examples
--------
.. manim-slides:: WithoutWaitExample
from manim import *
from manim_slides import Slide
class WithoutWaitExample(Slide):
def construct(self):
circle = Circle(radius=2)
arrow = Arrow().next_to(circle, RIGHT).scale(-1)
text = Text("Small\ngap").next_to(arrow, RIGHT)
self.play(Create(arrow), FadeIn(text))
self.play(Create(circle))
self.next_slide()
self.play(FadeOut(circle))
.. manim-slides:: WithWaitExample
from manim import *
from manim_slides import Slide
class WithWaitExample(Slide):
def construct(self):
self.wait_time_between_slides = 0.1 # A small value > 1 / FPS
circle = Circle(radius=2)
arrow = Arrow().next_to(circle, RIGHT).scale(-1)
text = Text("No more\ngap").next_to(arrow, RIGHT)
self.play(Create(arrow), FadeIn(text))
self.play(Create(circle))
self.next_slide()
self.play(FadeOut(circle))
"""
return self.__wait_time_between_slides
@wait_time_between_slides.setter
def wait_time_between_slides(self, wait_time: float) -> None:
self.__wait_time_between_slides = max(wait_time, 0.0)
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;
.. manim-slides:: NextSlideExample
from manim import *
from manim_slides import Slide
class NextSlideExample(Slide):
def construct(self):
text = Text("Hello World!")
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"
if self.wait_time_between_slides > 0.0:
self.wait(self.wait_time_between_slides)
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."""
self.slides.append(
if (
len(self.__slides) > 0
and self.__current_animation == self.__slides[-1].end_animation
):
self.__slides[-1].type = SlideType.last
return
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.
.. warning::
When rendered with RevealJS, loops cannot be in the first nor
the last slide.
Examples
--------
The following contains one slide that will loop endlessly.
.. manim-slides:: LoopExample
from manim import *
from manim_slides import Slide
class LoopExample(Slide):
def construct(self):
dot = Dot(color=BLUE, radius=1)
self.play(FadeIn(dot))
self.next_slide()
self.start_loop()
self.play(Indicate(dot, scale_factor=2))
self.end_loop()
self.play(FadeOut(dot))
"""
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)
files_folder = 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 = str(self)
scene_files_folder = files_folder / scene_name
scene_name = type(self).__name__
scene_files_folder = os.path.join(files_folder, scene_name)
old_animation_files = set()
if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder)
elif not use_cache:
shutil.rmtree(scene_files_folder)
os.mkdir(scene_files_folder)
else:
old_animation_files.update(os.listdir(scene_files_folder))
scene_files_folder.mkdir(parents=True, exist_ok=True)
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,
):
filename = os.path.basename(src_file)
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
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
dst_file = scene_files_folder / src_file.name
rev_file = scene_files_folder / f"{src_file.stem}_reversed{src_file.suffix}"
dst_file = os.path.join(scene_files_folder, filename)
# We only copy animation if it was not present
if filename in old_animation_files:
old_animation_files.remove(filename)
else:
if not use_cache or not dst_file.exists():
shutil.copyfile(src_file, dst_file)
# We only reverse video if it was not present
if rev_filename in old_animation_files:
old_animation_files.remove(rev_filename)
else:
rev_file = os.path.join(scene_files_folder, rev_filename)
if not use_cache or not rev_file.exists():
reverse_video_file(src_file, rev_file)
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"
f"Copied {len(files)} animations to '{scene_files_folder.absolute()}' and generated reversed animations"
)
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
slide_path = self.__output_folder / f"{scene_name}.json"
with open(slide_path, "w") as f:
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
PresentationConfig(
slides=self.__slides,
files=files,
resolution=self.__resolution,
background_color=self.__background_color,
).to_file(slide_path)
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
)
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")
@ -219,14 +552,187 @@ class Slide(Scene): # type:ignore
config["max_files_cached"] = max_files_cached
self.save_slides()
self.__save_slides()
def wipe(
self,
current: Sequence[Mobject] = [],
future: Sequence[Mobject] = [],
direction: np.ndarray = LEFT,
fade_in_kwargs: Mapping[str, Any] = {},
fade_out_kwargs: Mapping[str, Any] = {},
**kwargs: Any,
) -> AnimationGroup:
"""
Returns a wipe animation that will shift all the current objects outside
of the current scene's scope, and all the future objects inside.
:param current: A sequence of mobjects to remove from the scene.
:param future: A sequence of mobjects to add to the scene.
:param direction: The wipe direction.
:param fade_in_kwargs: Keyword arguments passed to
:class:`FadeIn<manim.animation.fading.FadeIn>`.
:param fade_out_kwargs: Keyword arguments passed to
:class:`FadeOut<manim.animation.fading.FadeOut>`.
:param kwargs: Keyword arguments passed to
:class:`AnimationGroup<manim.animation.composition.AnimationGroup>`.
Examples
--------
.. manim-slides:: WipeExample
from manim import *
from manim_slides import Slide
class WipeExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
square = Square()
text = Text("This is a wipe example").next_to(square, DOWN)
beautiful = Text("Beautiful, no?")
self.play(FadeIn(circle))
self.next_slide()
self.play(self.wipe(circle, Group(square, text)))
self.next_slide()
self.play(self.wipe(Group(square, text), beautiful, direction=UP))
self.next_slide()
self.play(self.wipe(beautiful, circle, direction=DOWN + RIGHT))
"""
shift_amount = np.asarray(direction) * np.array(
[self.__frame_width, self.__frame_height, 0.0]
)
animations = []
for mobject in future:
animations.append(FadeIn(mobject, shift=shift_amount, **fade_in_kwargs))
for mobject in current:
animations.append(FadeOut(mobject, shift=shift_amount, **fade_out_kwargs))
return AnimationGroup(*animations, **kwargs)
def zoom(
self,
current: Sequence[Mobject] = [],
future: Sequence[Mobject] = [],
scale: float = 4.0,
out: bool = False,
fade_in_kwargs: Mapping[str, Any] = {},
fade_out_kwargs: Mapping[str, Any] = {},
**kwargs: Any,
) -> AnimationGroup:
"""
Returns a zoom animation that will fade out all the current objects,
and fade in all the future objects. Objects are faded in a direction
that goes towards the camera.
:param current: A sequence of mobjects to remove from the scene.
:param future: A sequence of mobjects to add to the scene.
:param scale: How much the objects are scaled (up or down).
:param out: If set, the objects fade in the opposite direction.
:param fade_in_kwargs: Keyword arguments passed to
:class:`FadeIn<manim.animation.fading.FadeIn>`.
:param fade_out_kwargs: Keyword arguments passed to
:class:`FadeOut<manim.animation.fading.FadeOut>`.
:param kwargs: Keyword arguments passed to
:class:`AnimationGroup<manim.animation.composition.AnimationGroup>`.
Examples
--------
.. manim-slides:: ZoomExample
from manim import *
from manim_slides import Slide
class ZoomExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
square = Square()
self.play(FadeIn(circle))
self.next_slide()
self.play(self.zoom(circle, square))
self.next_slide()
self.play(self.zoom(square, circle, out=True, scale=10.))
"""
scale_in = 1.0 / scale
scale_out = scale
if out:
scale_in, scale_out = scale_out, scale_in
animations = []
for mobject in future:
animations.append(FadeIn(mobject, scale=scale_in, **fade_in_kwargs))
for mobject in current:
animations.append(FadeOut(mobject, scale=scale_out, **fade_out_kwargs))
return AnimationGroup(*animations, **kwargs)
class ThreeDSlide(Slide, ThreeDScene): # type: ignore
"""
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
Inherits from :class:`Slide` and :class:`ThreeDScene<manim.scene.three_d_scene.ThreeDScene>` and provide necessary tools for slides rendering.
Note that ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
.. note:: ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
Examples
--------
.. manim-slides:: ThreeDExample
from manim import *
from manim_slides import ThreeDSlide
class ThreeDExample(ThreeDSlide):
def construct(self):
title = Text("A 2D Text")
self.play(FadeIn(title))
self.next_slide()
sphere = Sphere([0, 0, -3])
self.move_camera(phi=PI/3, theta=-PI/4, distance=7)
self.play(
GrowFromCenter(sphere),
Transform(title, Text("A 3D Text"))
)
self.next_slide()
bye = Text("Bye!")
self.start_loop()
self.play(
self.wipe(
self.mobjects_without_canvas,
[bye],
direction=UP
)
)
self.wait(.5)
self.play(
self.wipe(
self.mobjects_without_canvas,
[title, sphere],
direction=DOWN
)
)
self.wait(.5)
self.end_loop()
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
"""
pass

View File

@ -1,6 +1,6 @@
import os
import sys
from functools import partial
from pathlib import Path
from typing import Any
import click
@ -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)
@ -69,7 +68,7 @@ class Wizard(QWidget): # type: ignore
self.layout = QGridLayout()
for i, (key, value) in enumerate(self.config.dict().items()):
for i, (key, value) in enumerate(self.config.keys.dict().items()):
# Create label for key name information
label = QLabel()
key_info = value["name"] or key
@ -84,7 +83,7 @@ class Wizard(QWidget): # type: ignore
)
self.buttons.append(button)
button.clicked.connect(
partial(self.openDialog, i, getattr(self.config, key))
partial(self.openDialog, i, getattr(self.config.keys, key))
)
self.layout.addWidget(button, i, 1)
@ -103,7 +102,7 @@ class Wizard(QWidget): # type: ignore
def saveConfig(self) -> None:
try:
Config.parse_obj(self.config.dict())
Config.model_validate(self.config.dict())
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
@ -131,7 +130,7 @@ class Wizard(QWidget): # type: ignore
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: str, force: bool, merge: bool) -> None:
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@ -141,18 +140,18 @@ def wizard(config_path: str, force: bool, merge: bool) -> None:
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Initialize a new default configuration file."""
return _init(config_path, force, merge, skip_interactive=True)
def _init(
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Actual initialization code for configuration file, with optional interactive mode."""
if os.path.exists(config_path):
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
@ -176,8 +175,8 @@ def _init(
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if os.path.exists(config_path):
config = Config.parse_file(config_path)
if config_path.exists():
config = Config.from_file(config_path)
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides Wizard")
@ -188,9 +187,8 @@ def _init(
config = window.config
if merge:
config = Config.parse_file(config_path).merge_with(config)
config = Config.from_file(config_path).merge_with(config)
with open(config_path, "w") as config_file:
config_file.write(config.json(indent=2))
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")

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

5607
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +1,132 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["setuptools", "poetry-core>=1.0.0"]
[tool.black]
target-version = ["py38"]
[tool.isort]
profile = "black"
py_version = 38
[tool.mypy]
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
strict = true
[tool.poetry]
name = "manim-slides"
description = "Tool for live presentations using manim"
authors = [
"Jérome Eertmans <jeertmans@icloud.com>"
"Jérome Eertmans <jeertmans@icloud.com>"
]
version = "4.7.1"
license = "GPL-3.0-only"
readme = "README.md"
homepage = "https://github.com/jeertmans/manim-slides"
documentation = "https://eertmans.be/manim-slides"
repository = "https://github.com/jeertmans/manim-slides"
keywords = ["manim", "slides", "plugin", "manimgl"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering",
]
exclude = ["docs/","static/"]
packages = [
{ include = "manim_slides" },
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering"
]
description = "Tool for live presentations using manim"
documentation = "https://eertmans.be/manim-slides"
exclude = ["docs/", "static/"]
homepage = "https://github.com/jeertmans/manim-slides"
keywords = ["manim", "slides", "plugin", "manimgl"]
license = "MIT"
name = "manim-slides"
packages = [
{include = "manim_slides"}
]
readme = "README.md"
repository = "https://github.com/jeertmans/manim-slides"
version = "4.16.1"
[tool.poetry.dependencies]
python = ">=3.8,<3.12"
click = ">=8.0"
click-default-group = ">=1.2"
numpy = ">=1.19"
opencv-python = ">=4.6"
pydantic = ">=1.9"
pyside6 = ">=6"
requests = ">=2.26"
tqdm = ">=4.62"
click = "^8.1.3"
click-default-group = "^1.2.2"
docutils = {version = "^0.20.1", optional = true}
ipython = {version = ">=8.12.2", optional = true}
jinja2 = {version = "^3.1.2", optional = true}
lxml = "^4.9.2"
manim = {version = "^0.17.3", optional = true}
manimgl = {version = "^1.6.1", optional = true}
notebook = {version = "^7.0.2", optional = true}
numpy = "^1.19"
opencv-python = "^4.6.0.66"
pillow = "^9.5.0"
pydantic = "^2.0.1"
pydantic-extra-types = "^2.0.0"
pyside6 = "^6.5.1.1"
python = ">=3.8.1,<3.12"
python-pptx = "^0.6.21"
requests = "^2.28.1"
rich = "^13.3.2"
rtoml = "^0.9.0"
tqdm = "^4.64.1"
[tool.poetry.dev-dependencies]
manim = "^0.17.0"
[tool.poetry.extras]
magic = ["manim", "ipython"]
manim = ["manim"]
manimgl = ["manimgl"]
sphinx-directive = ["docutils", "jinja2", "manim"]
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
black = "^22.10.0"
bump2version = "^1.0.1"
isort = "^5.12.0"
mypy = "^0.991"
pre-commit = "^3.0.2"
ruff = "^0.0.219"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
furo = "^2023.5.20"
ipykernel = "^6.25.1"
manim = "^0.17.3"
myst-parser = "^2.0.0"
nbsphinx = "^0.9.2"
pandoc = "^2.3"
sphinx = "^7.0.1"
sphinx-click = "^4.4.0"
sphinx-copybutton = "^0.5.1"
sphinxext-opengraph = "^0.7.5"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies]
manim = "^0.17.3"
manimgl = "^1.6.1"
[build-system]
requires = ["setuptools","poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-env = "^0.8.2"
pytest-xdist = "^3.3.1"
[tool.poetry.plugins]
[tool.poetry.plugins."console_scripts"]
manim-slides = "manim_slides.__main__:cli"
[tool.pytest.ini_options]
env = [
"QT_QPA_PLATFORM=offscreen"
]
filterwarnings = [
"error",
"ignore::DeprecationWarning"
]
[tool.ruff]
ignore = [
"E501"
]
target-version = "py38"

BIN
static/docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

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

13
tests/conftest.py Normal file
View File

@ -0,0 +1,13 @@
from pathlib import Path
from typing import Iterator
import pytest
from manim_slides.logger import make_logger
_ = make_logger() # This is run so that "PERF" level is created
@pytest.fixture
def folder_path() -> Iterator[Path]:
yield (Path(__file__).parent / "slides").resolve()

View File

@ -0,0 +1,32 @@
{
"slides": [
{
"type": "slide",
"start_animation": 0,
"end_animation": 1,
"number": 1
},
{
"type": "loop",
"start_animation": 1,
"end_animation": 2,
"number": 2
},
{
"type": "last",
"start_animation": 2,
"end_animation": 3,
"number": 3
}
],
"files": [
"slides/files/BasicExample/1413466013_3346521118_223132457.mp4",
"slides/files/BasicExample/1672018281_3136302242_2191168284.mp4",
"slides/files/BasicExample/1672018281_1369283980_3942561600.mp4"
],
"resolution": [
1920,
1080
],
"background_color": "black"
}

142
tests/test_commons.py Normal file
View File

@ -0,0 +1,142 @@
from pathlib import Path
import click
import pytest
from click.testing import CliRunner
from manim_slides.commons import (
config_options,
config_path_option,
folder_path_option,
verbosity_option,
)
def test_config_options() -> None:
@click.command()
@config_options
def main(config_path: Path, force: bool, merge: bool) -> None:
pass
runner = CliRunner()
with runner.isolated_filesystem():
with open("config.json", "w") as f:
f.write("Hello world!")
result = runner.invoke(main, ["--config", "config.json", "--force", "--merge"])
assert result.exit_code == 0
result = runner.invoke(main, ["-c", "config.json", "-f", "-m"])
def test_config_path_option() -> None:
@click.command()
@config_path_option
def main(config_path: Path) -> None:
pass
runner = CliRunner()
with runner.isolated_filesystem() as temp_dir:
with open("config.json", "w") as f:
f.write("Hello world!")
result = runner.invoke(main, ["--config", "config.json"])
assert result.exit_code == 0
result = runner.invoke(main, ["-c", "config.json"])
assert result.exit_code == 0
result = runner.invoke(main, ["--config", "unexisting.json"])
assert result.exit_code == 0
result = runner.invoke(main, ["--config", "unexisting"])
assert result.exit_code == 0
result = runner.invoke(main, ["--config", temp_dir])
assert result.exit_code != 0
def test_folder_path_option() -> None:
@click.command()
@folder_path_option
def main(folder: Path) -> None:
pass
runner = CliRunner()
with runner.isolated_filesystem() as temp_dir:
with open("file.txt", "w") as f:
f.write("Hello world!")
result = runner.invoke(main, ["--folder", "file.txt"])
assert result.exit_code != 0
result = runner.invoke(main, ["--folder", "unexisting.txt"])
assert result.exit_code != 0
result = runner.invoke(main, ["--folder", "unexisting"])
assert result.exit_code != 0
result = runner.invoke(main, ["--folder", temp_dir])
assert result.exit_code == 0
@pytest.mark.parametrize(
("verbosity",),
[("PeRF",), ("DEBUG",), ("info",), ("waRNING",), ("eRRor",), ("CrItIcAl",)],
)
def test_valid_verbosity_option(verbosity: str) -> None:
@click.command()
@verbosity_option
def main() -> None:
pass
runner = CliRunner()
result = runner.invoke(main, ["-v", verbosity])
assert result.exit_code == 0
result = runner.invoke(main, ["--verbosity", verbosity])
assert result.exit_code == 0
with runner.isolation(env={"MANIM_SLIDES_VERBOSITY": verbosity}):
result = runner.invoke(main)
assert result.exit_code == 0
@pytest.mark.parametrize(
("verbosity",), [("test",), ("deebug",), ("warn",), ("errors",)]
)
def test_invalid_verbosity_option(verbosity: str) -> None:
@click.command()
@verbosity_option
def main() -> None:
pass
runner = CliRunner()
result = runner.invoke(main, ["-v", verbosity])
assert result.exit_code != 0
result = runner.invoke(main, ["--verbosity", verbosity])
assert result.exit_code != 0
with runner.isolation(env={"MANIM_SLIDES_VERBOSITY": verbosity}):
result = runner.invoke(main)
assert result.exit_code != 0

103
tests/test_config.py Normal file
View File

@ -0,0 +1,103 @@
import random
import string
import tempfile
from pathlib import Path
from typing import Any, Generator, List
import pytest
from pydantic import ValidationError
from manim_slides.config import (
Key,
PresentationConfig,
SlideConfig,
SlideType,
merge_basenames,
)
def random_path(
length: int = 20,
dirname: Path = Path("./media/videos/example"),
suffix: str = ".mp4",
touch: bool = False,
) -> Path:
basename = "".join(random.choices(string.ascii_letters, k=length))
filepath = dirname.joinpath(basename + suffix)
if touch:
filepath.touch()
return filepath
@pytest.fixture
def paths() -> Generator[List[Path], None, None]:
random.seed(1234)
yield [random_path() for _ in range(20)]
@pytest.fixture
def presentation_config(paths: List[Path]) -> Generator[PresentationConfig, None, None]:
dirname = Path(tempfile.mkdtemp())
files = [random_path(dirname=dirname, touch=True) for _ in range(10)]
slides = [
SlideConfig(
type=SlideType.slide,
start_animation=0,
end_animation=5,
number=1,
),
SlideConfig(
type=SlideType.loop,
start_animation=5,
end_animation=6,
number=2,
),
SlideConfig(
type=SlideType.last,
start_animation=6,
end_animation=10,
number=3,
),
]
yield PresentationConfig(
slides=slides,
files=files,
)
def test_merge_basenames(paths: List[Path]) -> None:
path = merge_basenames(paths)
assert path.suffix == paths[0].suffix
assert path.parent == paths[0].parent
class TestKey:
@pytest.mark.parametrize(("ids", "name"), [([1], None), ([1], "some key name")])
def test_valid_keys(self, ids: Any, name: Any) -> None:
_ = Key(ids=ids, name=name)
@pytest.mark.parametrize(
("ids", "name"), [([], None), ([-1], None), ([1], {"an": " invalid name"})]
)
def test_invalid_keys(self, ids: Any, name: Any) -> None:
with pytest.raises(ValidationError):
_ = Key(ids=ids, name=name)
class TestPresentationConfig:
def test_validate(self, presentation_config: PresentationConfig) -> None:
obj = presentation_config.model_dump()
_ = PresentationConfig.model_validate(obj)
def test_bump_to_json(self, presentation_config: PresentationConfig) -> None:
_ = presentation_config.model_dump_json(indent=2)
def test_empty_presentation_config(self) -> None:
with pytest.raises(ValidationError):
_ = PresentationConfig(slides=[], files=[])

131
tests/test_convert.py Normal file
View File

@ -0,0 +1,131 @@
from enum import EnumMeta
import pytest
from manim_slides.convert import (
PDF,
AutoAnimateEasing,
AutoAnimateMatcher,
AutoPlayMedia,
AutoSlideMethod,
BackgroundSize,
BackgroundTransition,
ControlsBackArrows,
ControlsLayout,
Converter,
Display,
JsBool,
JsFalse,
JsNull,
JsTrue,
KeyboardCondition,
NavigationMode,
PowerPoint,
PreloadIframes,
RevealJS,
RevealTheme,
ShowSlideNumber,
SlideNumber,
Transition,
TransitionSpeed,
)
@pytest.mark.parametrize(
("enum_type",),
[
(JsTrue,),
(JsFalse,),
(JsBool,),
(JsNull,),
(ControlsLayout,),
(ControlsBackArrows,),
(SlideNumber,),
(ShowSlideNumber,),
(KeyboardCondition,),
(NavigationMode,),
(AutoPlayMedia,),
(PreloadIframes,),
(AutoAnimateMatcher,),
(AutoAnimateEasing,),
(AutoSlideMethod,),
(Transition,),
(TransitionSpeed,),
(BackgroundSize,),
(BackgroundTransition,),
(Display,),
(RevealTheme,),
],
)
def test_format_enum(enum_type: EnumMeta) -> None:
for enum in enum_type: # type: ignore[var-annotated]
expected = str(enum)
got = f"{enum}"
assert expected == got
got = "{enum}".format(enum=enum)
assert expected == got
got = format(enum, "")
assert expected == got
@pytest.mark.parametrize(
("enum_type",),
[
(ControlsLayout,),
(ControlsBackArrows,),
(SlideNumber,),
(ShowSlideNumber,),
(KeyboardCondition,),
(NavigationMode,),
(AutoPlayMedia,),
(PreloadIframes,),
(AutoAnimateMatcher,),
(AutoAnimateEasing,),
(AutoSlideMethod,),
(Transition,),
(TransitionSpeed,),
(BackgroundSize,),
(BackgroundTransition,),
(Display,),
],
)
def test_quoted_enum(enum_type: EnumMeta) -> None:
for enum in enum_type: # type: ignore[var-annotated]
if enum in ["true", "false", "null"]:
continue
expected = "'" + enum.value + "'"
got = str(enum)
assert expected == got
@pytest.mark.parametrize(
("enum_type",),
[
(JsTrue,),
(JsFalse,),
(JsBool,),
(JsNull,),
(RevealTheme,),
],
)
def test_unquoted_enum(enum_type: EnumMeta) -> None:
for enum in enum_type: # type: ignore[var-annotated]
expected = enum.value
got = str(enum)
assert expected == got
class TestConverter:
@pytest.mark.parametrize(
("name", "converter"), [("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint)]
)
def test_from_string(self, name: str, converter: type) -> None:
assert Converter.from_string(name) == converter

15
tests/test_defaults.py Normal file
View File

@ -0,0 +1,15 @@
from pathlib import Path
from manim_slides.defaults import CONFIG_PATH, FFMPEG_BIN, FOLDER_PATH
def test_folder_path() -> None:
assert FOLDER_PATH == Path("./slides")
def test_config_path() -> None:
assert CONFIG_PATH == Path(".manim-slides.toml")
def test_ffmpeg_bin() -> None:
assert FFMPEG_BIN == Path("ffmpeg")

93
tests/test_main.py Normal file
View File

@ -0,0 +1,93 @@
from pathlib import Path
from click.testing import CliRunner
from manim_slides.__main__ import cli
def test_help() -> None:
runner = CliRunner()
results = runner.invoke(cli, ["-S", "--help"])
assert results.exit_code == 0
results = runner.invoke(cli, ["-S", "-h"])
assert results.exit_code == 0
def test_defaults_to_present(folder_path: Path) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
cli, ["BasicExample", "--folder", str(folder_path), "-s"]
)
assert results.exit_code == 0
def test_present(folder_path: Path) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
cli, ["present", "BasicExample", "--folder", str(folder_path), "-s"]
)
assert results.exit_code == 0
def test_convert(folder_path: Path) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"convert",
"BasicExample",
"basic_example.html",
"--folder",
str(folder_path),
],
)
assert results.exit_code == 0
def test_init() -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"init",
"--force",
],
)
assert results.exit_code == 0
def test_list_scenes(folder_path: Path) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"list-scenes",
"--folder",
str(folder_path),
],
)
assert results.exit_code == 0
assert "BasicExample" in results.output
def test_wizard() -> None:
# TODO
pass

143
tests/test_manim.py Normal file
View File

@ -0,0 +1,143 @@
import importlib
import sys
from contextlib import contextmanager
from importlib.abc import MetaPathFinder
from importlib.machinery import ModuleSpec
from types import ModuleType
from typing import Iterator, Optional, Sequence
import pytest
import manim_slides.manim as msm
@contextmanager
def suppress_module_finder() -> Iterator[None]:
meta_path = sys.meta_path
try:
class PathFinder(MetaPathFinder):
@classmethod
def find_spec(
cls,
fullname: str,
path: Optional[Sequence[str]],
target: Optional[ModuleType] = None,
) -> Optional[ModuleSpec]:
if fullname in ["manim", "manimlib"]:
return None
for finder in meta_path:
spec = finder.find_spec(fullname, path, target=target)
if spec is not None:
return spec
return None
sys.meta_path = [PathFinder]
yield
finally:
sys.meta_path = meta_path
def assert_import(
*,
manim: bool,
manim_available: bool,
manim_imported: bool,
manimgl: bool,
manimgl_available: bool,
manimgl_imported: bool,
) -> None:
importlib.reload(msm)
assert msm.MANIM == manim
assert msm.MANIM_AVAILABLE == manim_available
assert msm.MANIM_IMPORTED == manim_imported
assert msm.MANIMGL == manimgl
assert msm.MANIMGL_AVAILABLE == manim_available
assert msm.MANIMGL_IMPORTED == manimgl_imported
@pytest.mark.filterwarnings("ignore:assert_import")
def test_manim_and_manimgl_imported() -> None:
import manim # noqa: F401
import manimlib # noqa: F401
assert_import(
manim=True,
manim_available=True,
manim_imported=True,
manimgl=False,
manimgl_available=True,
manimgl_imported=True,
)
def test_manim_imported() -> None:
import manim # noqa: F401
if "manimlib" in sys.modules:
del sys.modules["manimlib"]
assert_import(
manim=True,
manim_available=True,
manim_imported=True,
manimgl=False,
manimgl_available=True,
manimgl_imported=False,
)
def test_manimgl_imported() -> None:
import manimlib # noqa: F401
if "manim" in sys.modules:
del sys.modules["manim"]
assert_import(
manim=False,
manim_available=True,
manim_imported=False,
manimgl=True,
manimgl_available=True,
manimgl_imported=True,
)
def test_nothing_imported() -> None:
if "manim" in sys.modules:
del sys.modules["manim"]
if "manimlib" in sys.modules:
del sys.modules["manimlib"]
assert_import(
manim=True,
manim_available=True,
manim_imported=False,
manimgl=False,
manimgl_available=True,
manimgl_imported=False,
)
def test_no_package_available() -> None:
with suppress_module_finder():
if "manim" in sys.modules:
del sys.modules["manim"]
if "manimlib" in sys.modules:
del sys.modules["manimlib"]
with pytest.raises(ModuleNotFoundError):
# Actual values are not important
assert_import(
manim=False,
manim_available=False,
manim_imported=False,
manimgl=False,
manimgl_available=False,
manimgl_imported=False,
)

107
tests/test_slide.py Normal file
View File

@ -0,0 +1,107 @@
import pytest
from manim import Text
from pydantic import ValidationError
from manim_slides.slide import Slide
def assert_construct(cls: type) -> type:
class Wrapper:
@classmethod
def test_construct(_) -> None:
cls().construct()
return Wrapper
class TestSlide:
@assert_construct
class TestLoop(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
self.start_loop()
self.play(text.animate.scale(2))
self.end_loop()
with pytest.raises(AssertionError):
self.end_loop()
self.start_loop()
with pytest.raises(AssertionError):
self.start_loop()
with pytest.raises(ValidationError):
self.end_loop()
@assert_construct
class TestWipe(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.add(text)
assert text in self.mobjects
assert bye not in self.mobjects
self.play(self.wipe([text], [bye]))
assert text not in self.mobjects
assert bye in self.mobjects
@assert_construct
class TestZoom(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.add(text)
assert text in self.mobjects
assert bye not in self.mobjects
self.play(self.zoom([text], [bye]))
assert text not in self.mobjects
assert bye in self.mobjects
@assert_construct
class TestCanvas(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert len(self.canvas) == 0
self.add(text)
assert len(self.canvas) == 0
self.add_to_canvas(text=text)
assert len(self.canvas) == 1
self.add(bye)
assert len(self.canvas) == 1
assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
self.remove(text)
assert len(self.canvas) == 1
self.add_to_canvas(bye=bye)
assert len(self.canvas) == 2
self.remove_from_canvas("text", "bye")
assert len(self.canvas) == 0
with pytest.raises(KeyError):
self.remove_from_canvas("text")