Compare commits

...

71 Commits

Author SHA1 Message Date
1dbd2fdde5 chore(version): bump 5.1.2 to 5.1.3 2024-02-16 13:58:43 +01:00
07fd2bdcf1 chore(lib): warn about non-preferred-qt-version (#373)
Closes #371
2024-02-16 13:57:54 +01:00
d586dab102 [pre-commit.ci] pre-commit autoupdate (#372)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-13 08:48:54 +01:00
0ff1f37475 [pre-commit.ci] pre-commit autoupdate (#369)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0)

* [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>
2024-02-06 08:48:42 +01:00
92c569950c Fix link to documentation in README. (#368)
* Fix link to documentation in README.

* [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>
2024-02-02 09:54:47 +01:00
648d7ff921 chore(version): bump 5.1.1 to 5.1.2 2024-02-01 13:02:40 +01:00
b47068ede5 fix(rtd): remove flyout in iframes (#367)
* fix(rtd): remove flyout in iframes

* fix(rtd): remove dirhtml and default to html

Because dirhtml seems to have issues with iframes

* fix(html): correct css style
2024-02-01 12:27:24 +01:00
973522a2ac chore(deps): bump codecov/codecov-action from 3 to 4 (#366)
* chore(deps): bump codecov/codecov-action from 3 to 4

Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-01 08:01:22 +01:00
2f82ca3409 chore(rtd): change builder 2024-01-31 15:55:36 +01:00
e208cced03 chore(README): update docs links 2024-01-31 14:50:30 +01:00
abbe577aae chore(version): bump 5.1.0 to 5.1.1 2024-01-31 14:43:25 +01:00
38ef91d30c chore(CHANGELOG): add entry about RTD 2024-01-31 14:41:42 +01:00
b17fd5409f chore(docs): use RTD (#365)
* chore(docs): use RTD

* chore(ci): remove pages deployment
2024-01-31 13:36:53 +01:00
186badba03 chore(docs): add Binder badge 2024-01-29 09:12:55 +01:00
39816d4994 chore(docs): update changelog 2024-01-26 18:21:38 +01:00
9cb1dae990 chore(version): bump to 5.1.0 2024-01-26 18:19:09 +01:00
9e4c1d76ee fix(ci): upload from ghcr 2024-01-26 17:04:18 +01:00
0316adf69e fix(ci): was-it just a typo? 2024-01-26 16:13:09 +01:00
fd9b06b955 chore(version): bump 5.1.0-rc3 to 5.1.0-rc4 2024-01-26 15:14:25 +01:00
16f740d2ad chore(deps): make Qt backend optional (#350)
* chore(deps): make Qt backend optional

TODO:
- [ ] Add relevant entry in CHANGELOG
- [ ] Update install documentation
- [ ] Make sure `manim-slides convert` can run without any Qt backend
- [ ] Make sure test suite works (partially) without any Qt backend
- [ ] Make sure we can import `manim_slides` without any Qt backend

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

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

* chore(deps): some fixes but wip

* chore(docs): update

* chore(deps): support PyQt6

* chore(deps): make Qt backend optional

TODO:
- [ ] Add relevant entry in CHANGELOG
- [ ] Update install documentation
- [ ] Make sure `manim-slides convert` can run without any Qt backend
- [ ] Make sure test suite works (partially) without any Qt backend
- [ ] Make sure we can import `manim_slides` without any Qt backend

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

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

* chore(deps): some fixes but wip

* chore(docs): update

* chore(deps): support PyQt6

* fix(deps): ci and docs

* fix(lib): missing package

* chore(ci): does it work?

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

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

* chore(test): skip failing

* chore(docs): update

* chore(docs): update

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

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

* fix(docs): typo

* fix(test): quit instead of shutdown

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-26 15:08:23 +01:00
f260d0d310 chore(deps): bump ssciwr/setup-mesa-dist-win from 1 to 2 (#361)
Bumps [ssciwr/setup-mesa-dist-win](https://github.com/ssciwr/setup-mesa-dist-win) from 1 to 2.
- [Release notes](https://github.com/ssciwr/setup-mesa-dist-win/releases)
- [Commits](https://github.com/ssciwr/setup-mesa-dist-win/compare/v1...v2)

---
updated-dependencies:
- dependency-name: ssciwr/setup-mesa-dist-win
  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>
2024-01-26 12:00:39 +01:00
39ff80ed1e chore(deps): bump pdm-project/setup-pdm from 3 to 4 (#360)
Bumps [pdm-project/setup-pdm](https://github.com/pdm-project/setup-pdm) from 3 to 4.
- [Release notes](https://github.com/pdm-project/setup-pdm/releases)
- [Commits](https://github.com/pdm-project/setup-pdm/compare/v3...v4)

---
updated-dependencies:
- dependency-name: pdm-project/setup-pdm
  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>
2024-01-26 12:00:25 +01:00
b84a22d0f9 [pre-commit.ci] pre-commit autoupdate (#358)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.13 → v0.1.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.13...v0.1.14)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-23 11:07:55 +01:00
7ba47728ff chore(docs): document HTML custom templates (#357)
* chore(docs): document HTML custom templates

Shows an example of custom template.

TODO:
- [ ] finish documentating;
- [ ] add possibility to pass `-cargs` to Manim Slides' `convert` method when calling the Sphinx extension.

Closes #356

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

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

* chore(docs): document changes and fix

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-23 11:07:36 +01:00
02a8173ed1 fix(lib): reversed video quality 2024-01-19 13:27:40 +01:00
07dff32be3 chore(ci): rename 2024-01-18 23:36:02 +01:00
e60edcc960 fix(ci): remove duplicate info 2024-01-18 23:34:07 +01:00
c1d2dc26b2 fix(ci): avoid dynamic if does not work 2024-01-18 23:33:00 +01:00
0db4a8c260 fix(ci): update 2024-01-18 23:24:49 +01:00
cb4f6f552c fix(docs): typos 2024-01-18 22:20:55 +01:00
9cd50e73b2 chore(docs): nicer placement 2024-01-18 22:15:04 +01:00
cea3d0b0c3 chore(version): bump 5.1.0-rc2 to 5.1.0-rc3 2024-01-18 22:07:43 +01:00
75126d8bab fix(docs): typo 2024-01-18 22:04:10 +01:00
be227bbdea chore(deps): use PDM instead of Poetry (#354)
* chore(deps): use PDM instead of Poetry

* chore(git): move to top

* chore(lint): fixes

* fix(pdm): dynamic version did not work

* chore(ci): fix syntax for Windows

* fix(ci): typo

* wip(docs): updates

* chore(docs): add changelog entry

* chore(docs): admonitions

* chore(docs): update to use manim-slides render

* chore(docs): make it better
2024-01-18 22:02:53 +01:00
62560ea41f chore(ci): create Docker image (#355)
* chore(ci): create Docker image

* fix(ci): run only if tag is pushed

* chore(docs): add changelog entry
2024-01-18 19:57:35 +01:00
4b8f90c1fa chore(LICENSE): update date 2024-01-18 19:04:45 +01:00
8b1c45b84e [pre-commit.ci] pre-commit autoupdate (#352)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.11...v0.1.13)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-18 12:02:58 +01:00
34cc66d8be chore(deps): bump actions/cache from 3 to 4 (#353)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  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>
2024-01-18 11:50:35 +01:00
57e11a0ea7 chore(deps): bump jinja2 from 3.1.2 to 3.1.3 (#349)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-12 16:32:36 +01:00
de342b32bb [pre-commit.ci] pre-commit autoupdate (#347)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.1.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.1.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-08 23:06:49 +01:00
6bd431d748 chore(lib): remove useless var 2024-01-04 13:06:56 +01:00
2e996c03a7 chore(deps): bump actions/deploy-pages from 3 to 4 (#342)
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 3 to 4.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v3...v4)

---
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>
2024-01-04 13:06:13 +01:00
d9715ccb96 chore(deps): bump actions/upload-pages-artifact from 2 to 3 (#343)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v2...v3)

---
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>
2024-01-04 13:06:06 +01:00
e3b3dd677f chore(deps): bump github/codeql-action from 2 to 3 (#338)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  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>
2024-01-04 13:05:59 +01:00
db91de1412 [pre-commit.ci] pre-commit autoupdate (#337)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.11.0 → v2.12.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.11.0...v2.12.0)
- [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.9)
- [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0)

* [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>
2024-01-04 13:05:43 +01:00
bf67a7b695 chore(deps): bump actions/upload-artifact from 3 to 4 (#339)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
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-12-15 11:48:24 +01:00
ea2d352fc1 chore(lib): do not include reversed files in HTML assets (#336)
* chore(lib): do not include reversed files in HTML assets

* [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-12-11 13:58:39 +01:00
74ddefe519 chore(deps): remove subprocess calls to FFMPEG with av (#335)
* chore(deps): remove subprocess calls to FFMPEG with `av`

Linked to the progess made in https://github.com/ManimCommunity/manim/pull/3501.

The following PR aims at reducing subprocess calls for security and speed reasons. Also, with https://github.com/ManimCommunity/manim/pull/3501 is merged, FFMPEG should not be needed anymore as it is part of `PyAv`.

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

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

* fix(lib): oops forgot to commit those changes

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

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

* chore(lib): clear assets and clean code

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

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

* chore(doc): document changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-11 13:53:58 +01:00
20b7ef4110 chore(deps): bump actions/deploy-pages from 2 to 3 (#329)
* chore(deps): bump actions/deploy-pages from 2 to 3

Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 2 to 3.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v2...v3)

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

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-11 13:37:07 +01:00
19a31a9136 chore(deps): bump actions/configure-pages from 3 to 4 (#330)
* chore(deps): bump actions/configure-pages from 3 to 4

Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 3 to 4.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v3...v4)

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

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-11 13:36:55 +01:00
d2925340aa chore(deps): bump actions/setup-python from 4 to 5 (#333)
* chore(deps): bump actions/setup-python from 4 to 5

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-07 08:36:32 +01:00
afeaa0d793 chore(version): bump 5.1.0-rc1 to 5.1.0-rc2 2023-12-05 13:29:43 +01:00
af3c4971ae feat(lib): add return_animation option (#331)
* feat(lib): add `return_animation` option

Allow to return animation instead of playing it.

* fix(lib): docs issue

* fix(ci): build with Python 3.10
2023-12-05 13:29:16 +01:00
b3ed127e31 chore(repo): delete unwanted file 2023-11-28 15:56:52 +01:00
fc200f22f5 chore(version): bump 5.0.0 to 5.1.0-rc1
chore(version): bump 5.0.0-rc1 to 5.1.0

chore(version): bump 5.1.0 to 5.1.0-rc1
2023-11-28 15:21:03 +01:00
050ee0ae78 feat(lib): enhance notes support (#324)
* feat(lib): enhance notes support

TODO: fix keyboard inputs and window order with `present`.

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

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

* chore(lint): allow too complex

* wip: presenter mode

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

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

* feat(cli): add presenter view

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-28 15:15:56 +01:00
a9b8081167 [pre-commit.ci] pre-commit autoupdate (#326)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-28 09:48:44 +01:00
dc58d498a8 feat(cli): automatically detect conversion format from DEST (#325)
* feat(cli): automatically detect conversion format from DEST

As titled

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

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

* fix(cli): fix and add tests

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

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

* fix(ci): ?

* [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-11-27 16:13:40 +01:00
f898dd3054 feat(lib): add support for presenter notes (#322)
* feat(lib): add support for presenter notes

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

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

* fix(test): typo

* Update test_slide.py

* Update convert.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-23 18:20:10 +01:00
b09a000c17 chore(lib): simplify how to add config options (#321)
* chore(lib): simplify how to add config options

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

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

* chore(lint): some fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-23 15:38:49 +01:00
eb8efa8e3d feat(lib): add playback rate config options (#320)
* feat(lib): add playback rate config options

Basic playback rate config options, closes #309

* [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-11-23 12:26:25 +01:00
1f0c94dc5c fix(cli): typo 2023-11-23 00:10:56 +01:00
2771aac4d0 chore(cli): reduce timeout for version check 2023-11-22 18:28:57 +01:00
ce799aeded feat(cli): add render command as alias to manim render (#317)
* feat(cli): add render command as alias to `manim render`

* feat(cli): add render command as alias to `manim render`

* chore(test): add test

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

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

* chore(lib): simplify how ManimGL rendering is called

* chore(lib): test

* chore(ci): try identify error

* wip

* chore(test): add result output

* try: checkout

* Revert "try: checkout"

This reverts commit 60985f0b612ca0c6bff9726ebcccf07b1b1c4832.

* fix(deps): don't update lock

* chore(lib): simplify code and improve usage

* chore(cli): better cli

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

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

* fix: does this fix?

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-22 18:26:44 +01:00
891273b2fc [pre-commit.ci] pre-commit autoupdate (#318)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-21 17:42:41 +01:00
2b25b6a89d chore(docs): warn users about #314 (#319)
* chore(docs): warn users about #314

Closes #316

* chore(lint): apply suggested changes
2023-11-21 17:37:30 +01:00
fc594533e9 [pre-commit.ci] pre-commit autoupdate (#310)
updates:
- [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0)
- [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.5)
- [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-19 10:42:18 +01:00
422102524f fix(lib): remove dummy content 2023-11-16 13:07:29 +01:00
739cbcee0a feat(cli): added --hide-info-window option (#313)
* feat(cli): added `--hide-info-window` option

This was suggested by Discord use #dung_14424 on the Manim Discord.

* [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-11-16 12:51:59 +01:00
3b3e3109a3 [pre-commit.ci] pre-commit autoupdate (#306)
updates:
- [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1)
- [github.com/astral-sh/ruff-pre-commit: v0.1.1 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.1...v0.1.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-12 11:01:36 +01:00
8921c3b8f9 chore(README): fix relative link for Windows image 2023-11-01 10:06:00 +01:00
60 changed files with 5737 additions and 4972 deletions

View File

@ -1,7 +1,7 @@
[bumpversion]
current_version = 5.0.0
current_version = 5.1.3
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?
serialize =
serialize =
{major}.{minor}.{patch}-rc{release}
{major}.{minor}.{patch}
commit = True
@ -11,10 +11,6 @@ message = chore(version): bump {current_version} to {new_version}
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

@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@ -24,7 +24,7 @@ jobs:
# This should be the path to the paper within your repo.
paper-path: paper/paper.md
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: paper
# This is the output path where Pandoc will write the compiled

13
.github/workflows/latest_tag.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Keep the versions up-to-date
on:
release:
types: [published, edited]
jobs:
actions-tagger:
runs-on: windows-latest
steps:
- uses: Actions-R-Us/actions-tagger@latest
with:
publish_latest_tag: true

View File

@ -1,86 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: [main]
pull_request:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: pages
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
permissions: write-all
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- 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: docs/media
key: ${{ runner.os }}-docs-media
- 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: docs/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

71
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: Upload Packages
on:
push:
release:
types: [published]
jobs:
publish-python:
name: Publish Python package
runs-on: ubuntu-latest
environment: release
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: '3.10'
cache: true
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
run: pdm publish
publish-docker:
name: Publish Docker image
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: create_release
shell: python
env:
tag_act: ${{ github.ref }}
run: |
import os
ref_tag = os.getenv('tag_act').split('/')[-1]
with open(os.getenv('GITHUB_OUTPUT'), 'w') as f:
print(f"tag_name={ref_tag}", file=f)
- name: Build and push
uses: docker/build-push-action@v5
with:
platforms: linux/arm64,linux/amd64
file: docker/Dockerfile
push: true
tags: |
ghcr.io/jeertmans/manim-slides:latest
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}

View File

@ -1,34 +0,0 @@
name: Upload Python Package
on:
push:
release:
types: [published]
jobs:
build_and_release:
name: Build and release
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- 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

@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.8', '3.9', '3.10', '3.11']
pyversion: ['3.9', '3.10', '3.11', '3.12']
runs-on: ${{ matrix.os }}
env:
QT_QPA_PLATFORM: offscreen
@ -25,14 +25,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.pyversion }}
cache: poetry
cache: true
# Path related stuff
- name: Append to Path on MacOS
@ -68,23 +65,23 @@ jobs:
- name: Install Mesa
if: matrix.os == 'windows-latest'
uses: ssciwr/setup-mesa-dist-win@v1
uses: ssciwr/setup-mesa-dist-win@v2
- name: Install Manim Slides
run: |
poetry install --with test --all-extras
pdm sync -Ggithub-action -Gtest
- name: Run pytest
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
run: poetry run pytest
run: pdm run pytest
- name: Run pytest and coverage
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
run: poetry run pytest --cov-report xml --cov=manim_slides tests/
run: pdm run pytest --cov-report xml --cov=manim_slides tests/
- name: Upload to codecov.io
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ __pycache__/
/build
/dist
*.egg-info/
.pdm-python
# Manim files
images/

View File

@ -7,30 +7,25 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.11.0
rev: v2.12.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.10.0
args: [--autofix, --trailing-commas]
- repo: https://github.com/keewis/blackdoc
rev: v0.3.9
hooks:
- id: black
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies:
- black==23.9.1
- id: blackdoc
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.1
rev: v0.2.1
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

23
.readthedocs.yaml Normal file
View File

@ -0,0 +1,23 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: '3.10'
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_install:
- ipython kernel install --name "manim-slides" --user
sphinx:
builder: html
configuration: docs/source/conf.py
fail_on_warning: true
python:
install:
- method: pip
path: .
extra_requirements:
- docs
- magic
- sphinx-directive

View File

@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.0...HEAD)
(v5.1.1)=
## [v5.1.1](https://github.com/jeertmans/manim-slides/compare/v5.1.0...v5.1.1)
(v5.1.1-chore)=
### Chore
- Move documentation to ReadTheDocs for better versioning.
[#365](https://github.com/jeertmans/manim-slides/pull/365)
(v5.1)=
## [v5.1](https://github.com/jeertmans/manim-slides/compare/v5.0.0...v5.1.0)
(v5.1-added)=
### Added
- Added the `--hide-info-window` option to `manim-slides present`.
[#313](https://github.com/jeertmans/manim-slides/pull/313)
- Added the `manim-slides render` command
to render slides using correct Manim installation.
[#317](https://github.com/jeertmans/manim-slides/pull/317)
- Added the `playback-rate` and `reversed-playback-rate` options
to slide config.
[#320](https://github.com/jeertmans/manim-slides/pull/320)
- Added the speaker notes option.
[#322](https://github.com/jeertmans/manim-slides/pull/322)
- Added `auto` option for conversion format, which is the default.
This is somewhat a **breaking change**, but changes to the CLI
API are not considered to be very important.
[#325](https://github.com/jeertmans/manim-slides/pull/325)
- Added `return_animation` option to slide animations `self.wipe`
and `self.zoom`.
[#331](https://github.com/jeertmans/manim-slides/pull/331)
- Created a Docker image, published on GitHub.
[#355](https://github.com/jeertmans/manim-slides/pull/355)
- Added `:template:` and `:config_options` options to
the Sphinx directive.
[#357](https://github.com/jeertmans/manim-slides/pull/357)
(v5.1-modified)=
### Modified
- Modified the internal logic to simplify adding configuration options.
[#321](https://github.com/jeertmans/manim-slides/pull/321)
- Remove `reversed` file assets when exporting to HTML, as it was not used.
[#336](https://github.com/jeertmans/manim-slides/pull/336)
(v5.1-chore)=
### Chore
- Removed subrocess calls to FFMPEG with direct `libav` bindings using
the `av` Python module. This should enhance rendering speed and security.
[#335](https://github.com/jeertmans/manim-slides/pull/335)
- Changed build backend to PDM and reflected on docs.
[#354](https://github.com/jeertmans/manim-slides/pull/354)
- Dropped Python 3.8 support.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Made Qt backend optional and support PyQt6 too.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Documentated how to create and use a custom HTML template.
[#357](https://github.com/jeertmans/manim-slides/pull/357)
## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0)
Prior to v5, there was no real CHANGELOG other than the GitHub releases,

View File

@ -29,4 +29,4 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.0.0
version: v5.1.3

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Jérome Eertmans
Copyright (c) 2022-2024 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

158
README.md
View File

@ -11,22 +11,22 @@
[![DOI][doi-badge]][doi-url]
[![JOSE Paper][jose-badge]][jose-url]
[![codecov][codecov-badge]][codecov-url]
[![Binder][binder-badge]][binder-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!
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)
* [Pip install](#pip-install)
* [Install From Repository](#install-from-repository)
- [Usage](#usage)
* [Basic Example](#basic-example)
* [Key Bindings](#key-bindings)
* [Interactive Tutorial](#interactive-tutorial)
* [Other Examples](#other-examples)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
@ -37,53 +37,21 @@ Tool for live presentations using either [Manim (community edition)](https://www
## 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:
```bash
pip install manim-slides
```
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
pip install manim-slides[manim] # For Manim
# or
pip install manim-slides[manimgl] # For ManimGL
```
[^1]: NOTE: you still need to have Manim or ManimGL platform-specific dependencies installed on your computer.
### Install From Repository
An alternative way to install Manim Slides is to clone the git repository, and install from there: read the [contributing guide](https://eertmans.be/manim-slides/contributing/workflow.html) to know how.
<!-- end install -->
Manim Slides requires either Manim or ManimGL to be installed, along
with their dependencies. Please checkout the
[documentation](https://eertmans.be/manim-slides/latest/installation.html)
for detailed install instructions.
## 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.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.
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
*PowerPoint* presentation.
The documentation is available [online](https://eertmans.be/manim-slides/).
@ -94,8 +62,6 @@ animations, and `self.next_slide(loop=True)` if you want the next slide to loop
over animations until the user presses continue:
```python
# example.py
from manim import * # or: from manimlib import *
from manim_slides import Slide
@ -119,68 +85,58 @@ class BasicExample(Slide):
First, render the animation files:
```bash
manim example.py BasicExample
# or
manimgl example.py BasicExample
manim-slides render example.py BasicExample
# or use ManimGL
manim-slides render --GL example.py BasicExample
```
<!-- end usage -->
To start the presentation using `Scene1`, `Scene2` and so on simply run:
> [!NOTE]
> Using `manim-slides render` makes sure to use the `manim`
> (or `manimlib`) library that was installed in the same Python environment.
> Put simply, this is a wrapper around
> `manim render [ARGS]...` (or `manimgl [ARGS]...`).
<!-- start more-usage -->
To start the presentation using `Scene1`, `Scene2` and so on, run:
```bash
manim-slides [OPTIONS] Scene1 Scene2...
```
Or in this example:
In our example:
```bash
manim-slides BasicExample
```
<!-- end usage -->
<!-- end more-usage -->
## Key Bindings
<p align="center">
<img alt="Example GIF" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif">
</p>
The default key bindings to control the presentation are:
<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:
```bash
manim-slides wizard
```
A default file can be created with:
```bash
manim-slides init
```
> **_NOTE:_** `manim-slides` uses key codes, which are platform dependent. Using the configuration wizard is therefore highly recommended.
For detailed usage documentation, run `manim-slides --help`, or go to the
[documentation](https://eertmans.be/manim-slides/reference/cli.html).
## Interactive Tutorial
Click on the image to watch a slides presentation that explains you how to use Manim Slides.
Click on the image to watch a slides presentation that explains to 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.
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)
## More Examples
More examples are available in the
[`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py)
file, if you downloaded the git repository.
## Comparison with Similar Tools
There exists are variety of tools that allows to create slides presentations containing Manim animations.
There exists a variety of tools that allows to create slides presentations
containing Manim animations.
Below is a comparison of the most used ones with Manim Slides:
@ -198,16 +154,23 @@ Below is a comparison of the most used ones with Manim Slides:
### How to increase quality on Windows
On Windows platform, one may encounter a lower image resolution than expected. Usually, this is observed because Windows rescales every application to fit the screen.
As found by [@arashash](https://github.com/arashash), in [#20](https://github.com/jeertmans/manim-slides/issues/20), the problem can be addressed by changing the scaling factor to 100%:
On Windows platform, one may encounter a lower image resolution than expected.
Usually, this is observed because Windows rescales every application to
fit the screen.
As found by [@arashash](https://github.com/arashash),
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
the problem can be addressed by changing the scaling factor to 100%:
![Windows Fix Scaling](static/windows_quality_fix.png)
<p align="center">
<img alt="Windows Fix Scaling" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png">
</p>
in *Settings*->*Display*.
## Contributing
Contributions are more than welcome! Please read through [our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
Contributions are more than welcome! Please read through
[our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
### Reporting an Issue
@ -262,12 +225,13 @@ you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
[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/
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
[documentation-url]: https://manim-slides.readthedocs.io/en/latest/?badge=latest
[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
[binder-badge]: https://mybinder.org/badge_logo.svg
[binder-url]: https://mybinder.org/v2/gh/jeertmans/manim-slides-binder/HEAD?filepath=getting_started.ipynb

55
docker/Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
FROM python:3.11-slim
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y \
ffmpeg \
build-essential \
gcc \
cmake \
libcairo2-dev \
libffi-dev \
libpango1.0-dev \
freeglut3-dev \
pkg-config \
make \
wget \
ghostscript
# setup a minimal texlive installation
COPY docker/texlive-profile.txt /tmp/
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/usr/local/texlive/bin/x86_64-linux:$PATH
RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz && \
mkdir /tmp/install-tl && \
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
&& tlmgr install \
amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
mathastext microtype ms physics preview ragged2e relsize rsfs \
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
# clone and build manim-slides
COPY . /opt/manim-slides
WORKDIR /opt/manim-slides
RUN pip install --no-cache manim[jupyterlab] .[sphinx-directive]
ARG NB_USER=manimslidesuser
ARG NB_UID=1000
ENV USER ${NB_USER}
ENV NB_UID ${NB_UID}
ENV HOME /manim-slides
RUN adduser --disabled-password \
--gecos "Default user" \
--uid ${NB_UID} \
${NB_USER}
# create working directory for user to mount local directory into
WORKDIR ${HOME}
USER root
RUN chown -R ${NB_USER}:${NB_USER} ${HOME}
RUN chmod 777 ${HOME}
USER ${NB_USER}
CMD [ "/bin/bash" ]

15
docker/README.md Normal file
View File

@ -0,0 +1,15 @@
# Docker Image
Manim Slides Docker image, highly inspired from the Manim Community Docker image.
Building the image can be done with:
```bash
docker build -t manim-slide/manin-slide:TAG -f docker/Dockerfile .
```
from the root directory of the repository.
> [!WARNING]
> If you run the command above from another place,
> Docker will not be able to find expected files.

View File

@ -0,0 +1,10 @@
selected_scheme scheme-minimal
TEXDIR /usr/local/texlive
TEXMFCONFIG ~/.texlive/texmf-config
TEXMFHOME ~/texmf
TEXMFLOCAL /usr/local/texlive/texmf-local
TEXMFSYSCONFIG /usr/local/texlive/texmf-config
TEXMFSYSVAR /usr/local/texlive/texmf-var
TEXMFVAR ~/.texlive/texmf-var
option_doc 0
option_src 0

View File

@ -0,0 +1,101 @@
<!doctype html>
<html>
<head>
<!-- Head stuff -->
</head>
<body>
<!-- Slides stuff -->
<script>
<!-- RevealJS stuff -->
</script>
<!-- Add a clock to each section dynamically using JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
var revealContainer = document.querySelector('.reveal');
// Append dynamic content to each section
var sections = revealContainer.querySelectorAll('.slides > section');
sections.forEach(function (section) {
// Create a new clock container
var clockContainer = document.createElement('div');
clockContainer.className = 'clock';
// Append the new clock container to the section
section.appendChild(clockContainer);
});
// Function to update the clock content
function updateClock() {
var now = new Date();
var hours = now.getHours();
var minutes = now.getMinutes();
var seconds = now.getSeconds();
// Format the time as HH:MM:SS
var timeString = pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
// Update the content of all clock containers
var clockContainers = document.querySelectorAll('.clock');
clockContainers.forEach(function (container) {
container.innerText = timeString;
});
}
// Function to pad zero for single-digit numbers
function pad(number) {
return String(number).padStart(2, "0");
}
// Update the clock every second
setInterval(updateClock, 1000);
// Register a reveal.js event to update the clock on each slide change
Reveal.addEventListener('slidechanged', function (event) {
updateClock();
});
// Initial update
updateClock();
});
</script>
<!-- define the style of the clock -->
<style>
.clock {
position: fixed;
bottom: 10px;
left: 10px;
font-size: 24px;
font-family: "Arial", sans-serif;
color: #333;
}
/* control the relative position of the clock to the slides */
.reveal .slides > section.present, .reveal .slides > section > section.present {
min-height: 100% !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
position: absolute !important;
top: 0 !important;
}
section > h1 {
position: absolute !important;
top: 0 !important;
margin-left: auto !important;
margin-right: auto !important;
left: 0 !important;
right: 0 !important;
}
.print-pdf .reveal .slides > section.present, .print-pdf .reveal .slides > section > section.present {
min-height: 770px !important;
position: relative !important;
}
</style>
</body>
</html>

View File

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

View File

@ -4,12 +4,20 @@
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import sys
from datetime import date
from manim_slides import __version__
assert sys.version_info >= (3, 10), "Building docs requires Python 3.10"
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Manim Slides"
copyright = "2023, Jérome Eertmans"
copyright = f"2024-{date.today().year}, Jérome Eertmans"
author = "Jérome Eertmans"
version = __version__
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -4,7 +4,7 @@ This document is there to help you recreate a working environment for Manim Slid
## Dependencies
```{include} ../../../README.md
```{include} ../installation.md
:start-after: <!-- start deps -->
:end-before: <!-- end deps -->
```
@ -20,64 +20,110 @@ Useful links:
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.
As for every Python project, using virtual environment is recommended to avoid
conflicts between modules.
For this project, we use [PDM](https://pdm-project.org/) to easily manage project
and development dependencies. If not already, please install this tool.
## Installing Python modules
With Poetry, installation becomes straightforward:
With PDM, installation becomes straightforward:
```bash
poetry install
pdm 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:
If you would like to install Manim or ManimGL,
as documented in the [quickstart](../quickstart),
you can use the `-G|--group` option:
```bash
poetry install --extras manim # For Manim
pdm install -Gmanim # For Manim
# or
poetry install --extras manimgl # For ManimGL
pdm install -Gmanimgl # For ManimGL
```
Additionnally, Manim Slides comes with group dependencies for development purposes:
Additionnally, Manim Slides comes with groups of dependencies for development purposes:
```bash
poetry install --with dev # For linters and formatters
pdm install -Gdev # For linters and formatters
# or
poetry install --with docs # To build the documentation locally
pdm install -Gdocs # To build the documentation locally
# or
pdm install -Gtest # To run tests
```
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.
You can also install everything with `pdm install -G:all`.
:::
## 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.:
Because modules are installed in a new Python environment,
you cannot use them directly in the shell.
Instead, you either need to prepend `pdm run` to any command, e.g.:
```bash
poetry run manim-slides wizard
pdm run manim-slides wizard
```
or enter a new shell that uses this new Python environment:
or [enter a new shell](https://pdm-project.org/latest/usage/venv/#activate-a-virtualenv)
that uses this new Python environment:
```
poetry shell
```bash
eval $(pdm venv activate) # Click on the link above to see shell-specific command
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.
Most of the tests are done with GitHub actions, thus not on your computer.
The only command you should run locally is:
```bash
pdm run 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.
For testing your code, simply run:
```bash
pdm run pytest
```
## Building the documentation
The documentation is generated using Sphinx, based on the content
in `docs/source` and in the `manim_slides` Python package.
To generate the documentation, run the following:
```bash
cd docs
pdm run make html
```
Then, the output index file is located at `docs/build/html/index.html` and
can be opened with any modern browser.
:::{warning}
Building the documentation can take quite some time, especially
the first time as it needs to render all the animations.
Further builds should run faster.
:::
## 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!
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

@ -26,7 +26,6 @@ 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.
@ -40,6 +39,7 @@ Manim Slides.
:hidden:
quickstart
installation
reference/index
features_table
manim_or_manimgl

157
docs/source/installation.md Normal file
View File

@ -0,0 +1,157 @@
# Installation
While installing Manim Slides and its dependencies on your global Python is fine,
we recommend using a virtual environment
(e.g., [venv](https://docs.python.org/3/tutorial/venv.html)) for a local installation.
Therefore, the following documentation will install Manim Slides using
[pipx](https://pipx.pypa.io/). This tool is a drop-in replacement
for installing Python packages that ship with one or more executable.
The benefit of using pipx is that it will automatically create a new virtual
environment for every package you install.
:::{note}
Everytime you read `pipx install`, you can use `pip install` instead,
if you are working in a virtual environment or else.
:::
## Dependencies
<!-- start deps -->
Manim Slides requires either Manim or ManimGL to be installed, along
with their dependencies.
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)
:::{warning}
If you install Manim from its git repository, as suggested by ManimGL,
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
for ManimGL), otherwise it might install an unsupported version of Manim!
See [#314](https://github.com/jeertmans/manim-slides/issues/314).
Also, note that ManimGL uses outdated dependencies, and may
not work out-of-the-box. One example is NumPy: ManimGL
does not specify any restriction on this package, but
only `numpy<1.25` will work, see
[#2053](https://github.com/3b1b/manim/issues/2053).
:::
<!-- end deps -->
## Pip Install
The recommended way to install the latest release
with all features is to use pipx:
```bash
pipx install -U "manim-slides[pyside6-full]"
```
:::{tip}
While not necessary, the `-U` indicates that we would
like to upgrade to the latest version available,
if Manim Slides is already installed.
:::
:::{note}
The quotes `"` are added because not all shell support unquoted
brackets (e.g., zsh) or commas (e.g., Windows).
:::
You can check that Manim Slides was correctly installed with:
```bash
manim-slides --version
```
## Custom install
If you want more control on what dependencies are installed,
you can always install the bare minimal dependencies with:
```bash
pipx install -U manim-slides
```
And install additional dependencies later.
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
pipx install -U "manim-slides[manim]" # For Manim
# or
pipx install -U "manim-slides[manimgl]" # For ManimGL
```
For optional dependencies documentation, see
[next section](#optional-dependencies).
:::{warning}
If you are installing with pipx, this is mandatory to at least include
either `manim` or `manimgl`.
:::
[^1]: You still need to have Manim or ManimGL platform-specific dependencies
installed on your computer.
## Optional dependencies
Along with the optional dependencies for Manim and ManimGL,
Manim Slides offers additional *extras*, that can be activated
using optional dependencies:
- `full`, to include `magic`, `manim`, `manimgl`, and
`sphinx-directive` extras (see below);
- `magic`, to include a Jupyter magic to render
animations inside notebooks. This automatically installs `manim`,
and does not work with ManimGL;
- `manim` and `manimgl`, for installing the corresponding
dependencies;
- `pyqt6` to include PyQt6 Qt bindings. Those bindings are available
on most platforms and Python version, but produce a weird black
screen between slide with `manim-slides present`,
see [#QTBUG-118501](https://bugreports.qt.io/browse/QTBUG-118501);
- `pyqt6-full` to include `full` and `pyqt6`;
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
on most platforms and Python version, except on Python 3.12[^2];
- `pyside6-full` to include `full` and `pyside6`;
- `sphinx-directive`, to generate presentation inside your Sphinx
documentation. This automatically installs `manim`,
and does not work with ManimGL.
Installing those extras can be done with the following syntax:
```bash
pipx install -U "manim-slides[extra1,extra2]"
```
[^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
observe the same visual bug as with PyQt6.
## When you need a Qt backend
Before `v5.1`, Manim Slides automatically included PySide6 as
a Qt backend. As only `manim-slides present` and `manim-slides wizard`
command need a graphical library, and installing PySide6 on all platforms
and Python version can be sometimes complicated, Manim Slides chooses
**not to include** any Qt backend.
The use can choose between PySide6 (best) and PyQt6, depending on their
availability and licensing rules.
As of `v5.1`, you **need** to have Qt bindings installed to use
`manim-slides present` or `manim-slides wizard`. The recommended way to
install those are via optional dependencies, as explained above.
## Install from source
An alternative way to install Manim Slides is to clone the git repository,
and build the package from source. Read the
[contributing guide](./contributing/workflow)
to know how to process.

View File

@ -1,11 +1,7 @@
# Quickstart
## Installation
```{include} ../../README.md
:start-after: <!-- start install -->
:end-before: <!-- end install -->
```
If not already, install Manim Slides, along with either Manim or ManimGL,
see [installation](./installation).
## Creating your first slides
@ -14,6 +10,19 @@
:end-before: <!-- end usage -->
```
:::{note}
Using `manim-slides render` makes sure to use the `manim`
(or `manimlib`) library that was installed in the same Python environment.
Put simply, this is a wrapper around
`manim render [ARGS]...` (or `manimgl [ARGS]...`).
:::
```{include} ../../README.md
:start-after: <!-- start more-usage -->
:end-before: <!-- end more-usage -->
```
The output slides should look this this:
```{eval-rst}

View File

@ -0,0 +1,71 @@
# Customize your RevealJS slides
One of the benefits of the `convert` command is the use of template files.
Currently, only the HTML export uses one. If not specified, the template
will be the one shipped with Manim Slides, see
[`manim_slides/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html).
Because you can actually use your own template with the `--use-template`
option, possibilities are infinite!
:::{warning}
Currently, the `PresentationConfig` class and its components
are not part of the public API. You can still use them, e.g.,
in the templates, but you may expect breaking changes between
releases.
Eventually, this will become part of the public API too,
and we will document its usage.
:::
## Adding a clock to each slide
In this example, we show how to add a self-updating clock
to the bottom left corner of every slide.
:::{note}
This example is inspired from
[@gsong-math's comment](https://github.com/jeertmans/manim-slides/issues/356#issuecomment-1902626943)
on Manim Slides' repository.
:::
### What to add
Whenever you want to create a template, it is best practice
to start from the default one (see link above).
Modifying it needs very basic HTML/JavaScript/CSS skills.
To add a clock, you can simply add the following to the
default template file:
```{eval-rst}
.. literalinclude:: ../_static/template.diff
:language: html
```
:::{tip}
Because we use RevealJS to generate HTML slides,
we recommend you to take a look at
[RevealJS' documentation](https://revealjs.com/).
:::
### How it renders
Then, using the `:template: <path/to/custom_template.html>`
option, the basic example renders as follows:
```{eval-rst}
.. manim-slides:: ../../../example.py:BasicExample
:hide_source:
:template: ../_static/template.html
```
### Full code
Below, you can read the full content of the template file.
```{eval-rst}
.. literalinclude:: ../_static/template.html
:language: html+jinja
```

View File

@ -7,6 +7,7 @@ Automatically generated reference for Manim Slides.
api
cli
customize_html
examples
gui
html

View File

@ -55,11 +55,11 @@
" Text(\"Press\"),\n",
" Text(\"and\"),\n",
" Text(\"loop\"),\n",
" ).arrange(DOWN, buff=1.)\n",
" \n",
" ).arrange(DOWN, buff=1.0)\n",
"\n",
" self.play(Write(text))\n",
" self.next_slide(loop=True)\n",
" self.play(Indicate(text[-1], scale_factor=2., run_time=.5))\n",
" self.play(Indicate(text[-1], scale_factor=2.0, run_time=0.5))\n",
" self.next_slide()\n",
" self.play(FadeOut(text))"
]

View File

@ -9,8 +9,10 @@ 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
:::{note}
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
@ -32,8 +34,8 @@ the key bindings might not be the same.
Example:
```bash
# If you use ManimGl, replace `manim` with `manimgl`
manim example.py BasicExample
# If you use ManimGl, add `--GL` after `render`
manim-slides render example.py BasicExample
# This or `manim-slides BasicExample` works since
# `present` is implied by default
@ -128,7 +130,6 @@ 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
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
@ -144,10 +145,11 @@ 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
:::{warning}
Keep in mind that playing large video files over the internet network
can take some time, and *glitches* may occur between slide transitions for this
reason.
:::
### Using the Github starter template

View File

@ -8,6 +8,7 @@ from .__version__ import __version__
from .convert import convert
from .logger import logger
from .present import list_scenes, present
from .render import render
from .wizard import init, wizard
@ -33,9 +34,7 @@ def cli(notify_outdated_version: bool) -> None:
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
try:
req_info: requests.models.Response = requests.get(
manim_info_url, timeout=10
)
req_info: requests.models.Response = requests.get(manim_info_url, timeout=2)
req_info.raise_for_status()
stable = req_info.json()["info"]["version"]
if stable != __version__:
@ -67,6 +66,7 @@ cli.add_command(convert)
cli.add_command(init)
cli.add_command(list_scenes)
cli.add_command(present)
cli.add_command(render)
cli.add_command(wizard)
if __name__ == "__main__":

View File

@ -1 +1 @@
__version__ = "5.0.0"
__version__ = "5.1.3"

View File

@ -1,6 +1,9 @@
import json
import shutil
from functools import wraps
from inspect import Parameter, signature
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
import rtoml
@ -14,7 +17,6 @@ from pydantic import (
model_validator,
)
from pydantic_extra_types.color import Color
from PySide6.QtCore import Qt
from .logger import logger
@ -35,6 +37,13 @@ class Signal(BaseModel): # type: ignore[misc]
receiver(*args)
def key_id(name: str) -> PositiveInt:
"""Avoid importing Qt too early."""
from qtpy.QtCore import Qt
return getattr(Qt, f"Key_{name}")
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
@ -70,14 +79,22 @@ class Key(BaseModel): # type: ignore[misc]
class Keys(BaseModel): # type: ignore[misc]
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
NEXT: Key = Key(ids=[Qt.Key_Right], name="NEXT")
PREVIOUS: Key = Key(ids=[Qt.Key_Left], name="PREVIOUS")
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
REPLAY: Key = Key(ids=[Qt.Key_R], name="REPLAY")
FULL_SCREEN: Key = Key(ids=[Qt.Key_F], name="TOGGLE FULL SCREEN")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
PLAY_PAUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("Space")], name="PLAY / PAUSE")
)
NEXT: Key = Field(default_factory=lambda: Key(ids=[key_id("Right")], name="NEXT"))
PREVIOUS: Key = Field(
default_factory=lambda: Key(ids=[key_id("Left")], name="PREVIOUS")
)
REVERSE: Key = Field(default_factory=lambda: Key(ids=[key_id("V")], name="REVERSE"))
REPLAY: Key = Field(default_factory=lambda: Key(ids=[key_id("R")], name="REPLAY"))
FULL_SCREEN: Key = Field(
default_factory=lambda: Key(ids=[key_id("F")], name="TOGGLE FULL SCREEN")
)
HIDE_MOUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("H")], name="HIDE / SHOW MOUSE")
)
@model_validator(mode="before")
@classmethod
@ -118,7 +135,7 @@ class Keys(BaseModel): # type: ignore[misc]
class Config(BaseModel): # type: ignore[misc]
"""General Manim Slides config."""
keys: Keys = Keys()
keys: Keys = Field(default_factory=Keys)
@classmethod
def from_file(cls, path: Path) -> "Config":
@ -135,11 +152,87 @@ class Config(BaseModel): # type: ignore[misc]
return self
class PreSlideConfig(BaseModel): # type: ignore
start_animation: int
end_animation: int
class BaseSlideConfig(BaseModel): # type: ignore
"""Base class for slide config."""
loop: bool = False
auto_next: bool = False
playback_rate: float = 1.0
reversed_playback_rate: float = 1.0
notes: str = ""
dedent_notes: bool = True
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
"""
Wrap a function to transform keyword argument into an instance of this class.
The function signature is updated to reflect the new keyword-only arguments.
The wrapped function must follow two criteria:
- its last parameter must be ``**kwargs`` (or equivalent);
- and its second last parameter must be ``<arg_name>``.
"""
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
@wraps(fun)
def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807
fun_kwargs = {
key: value
for key, value in kwargs.items()
if key not in cls.__fields__
}
fun_kwargs[arg_name] = cls(**kwargs)
return fun(*args, **fun_kwargs)
sig = signature(fun)
parameters = list(sig.parameters.values())
parameters[-2:-1] = [
Parameter(
field_name,
Parameter.KEYWORD_ONLY,
default=field_info.default,
annotation=field_info.annotation,
)
for field_name, field_info in cls.__fields__.items()
]
sig = sig.replace(parameters=parameters)
__wrapper__.__signature__ = sig # type: ignore[attr-defined]
return __wrapper__
return _wrapper_
@model_validator(mode="after")
@classmethod
def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig"
) -> "BaseSlideConfig":
if base_slide_config.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes)
return base_slide_config
class PreSlideConfig(BaseSlideConfig):
"""Slide config to be used prior to rendering."""
start_animation: int
end_animation: int
@classmethod
def from_base_slide_config_and_animation_indices(
cls,
base_slide_config: BaseSlideConfig,
start_animation: int,
end_animation: int,
) -> "PreSlideConfig":
return cls(
start_animation=start_animation,
end_animation=end_animation,
**base_slide_config.dict(),
)
@field_validator("start_animation", "end_animation")
@classmethod
@ -185,22 +278,17 @@ class PreSlideConfig(BaseModel): # type: ignore
return slice(self.start_animation, self.end_animation)
class SlideConfig(BaseModel): # type: ignore[misc]
class SlideConfig(BaseSlideConfig):
"""Slide config to be used after rendering."""
file: FilePath
rev_file: FilePath
loop: bool = False
auto_next: bool = False
@classmethod
def from_pre_slide_config_and_files(
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
) -> "SlideConfig":
return cls(
file=file,
rev_file=rev_file,
loop=pre_slide_config.loop,
auto_next=pre_slide_config.auto_next,
)
return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
class PresentationConfig(BaseModel): # type: ignore[misc]
@ -231,7 +319,9 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
with open(path, "w") as f:
f.write(self.model_dump_json(indent=2))
def copy_to(self, folder: Path, use_cached: bool = True) -> "PresentationConfig":
def copy_to(
self, folder: Path, use_cached: bool = True, include_reversed: bool = True
) -> "PresentationConfig":
"""Copy the files to a given directory."""
for slide_config in self.slides:
file = slide_config.file
@ -246,10 +336,7 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
if not use_cached or not dest.exists():
shutil.copy(file, dest)
if not use_cached or not rev_dest.exists():
if include_reversed and (not use_cached or not rev_dest.exists()):
shutil.copy(rev_file, rev_dest)
return self
DEFAULT_CONFIG = Config()

View File

@ -396,7 +396,7 @@ class RevealJS(Converter):
full_assets_dir.mkdir(parents=True, exist_ok=True)
for presentation_config in self.presentation_configs:
presentation_config.copy_to(full_assets_dir)
presentation_config.copy_to(full_assets_dir, include_reversed=False)
dest.parent.mkdir(parents=True, exist_ok=True)
@ -406,9 +406,17 @@ class RevealJS(Converter):
options = self.dict()
options["assets_dir"] = assets_dir
has_notes = any(
slide_config.notes != ""
for presentation_config in self.presentation_configs
for slide_config in presentation_config.slides
)
content = revealjs_template.render(
file_to_data_uri=file_to_data_uri,
get_duration_ms=get_duration_ms,
has_notes=has_notes,
env=os.environ,
**options,
)
@ -482,7 +490,7 @@ class PowerPoint(Converter):
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
def convert_to(self, dest: Path) -> None: # noqa: C901
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
prs.slide_width = self.width * 9525
@ -550,6 +558,9 @@ class PowerPoint(Converter):
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes
if self.auto_play_media:
auto_play_media(movie, loop=slide_config.loop)
@ -619,10 +630,11 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
@click.option(
"--to",
type=click.Choice(["html", "pdf", "pptx"], case_sensitive=False),
default="html",
type=click.Choice(["auto", "html", "pdf", "pptx"], case_sensitive=False),
metavar="FORMAT",
default="auto",
show_default=True,
help="Set the conversion format to use.",
help="Set the conversion format to use. Use 'auto' to detect format from DEST.",
)
@click.option(
"--open",
@ -637,14 +649,16 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"config_options",
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. E.g., pass `-cslide_number=true` to display slide numbers.",
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`.",
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
@ -663,7 +677,19 @@ def convert(
presentation_configs = get_scenes_presentation_config(scenes, folder)
try:
converter = Converter.from_string(to)(
if to == "auto":
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warn(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = Converter.from_string(to)
converter = cls(
presentation_configs=presentation_configs,
template=template,
**config_options,

View File

@ -2,4 +2,3 @@ from pathlib import Path
FOLDER_PATH: Path = Path("./slides")
CONFIG_PATH: Path = Path(".manim-slides.toml")
FFMPEG_BIN: Path = Path("ffmpeg")

View File

@ -21,7 +21,9 @@ This directive requires three additional dependencies:
with Sphinx.
You can install them manually, or with the extra keyword:
pip install manim-slides[sphinx-directive]
.. code-block:: bash
pip install "manim-slides[sphinx-directive]"
Note that you will still need to install Manim's platform-specific dependencies,
see
@ -69,6 +71,7 @@ render scenes that are defined within doctests, for example::
>>> class DirectiveDoctestExample(Slide):
... def construct(self):
... self.play(Create(dot))
...
A third application is to render scenes from another specific file::
@ -114,12 +117,71 @@ directive:
A list of methods, separated by spaces,
that is rendered in a reference block after the source code.
template
A path to the template file to use.
config_options
An unprocessed string of options to pass to ``manim-slides convert``.
Options must be separated with a space, and each option must be
a key, value pair using an equal sign as a separator.
Unlike for the CLI version, you don't need to prepend each option with
``-c``.
E.g., pass ``slide_number=true controls=false``.
By default, ``controls=true`` is set.
Examples
--------
The following code::
.. manim-slides:: MySlide
:hide_source:
:config_options: slide_number=true controls=false
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
text = Text("Hello")
self.wipe([], text)
self.next_slide()
self.play(text.animate.scale(2))
self.next_slide()
self.zoom(text)
Renders as follows:
.. manim-slides:: MySlide
:hide_source:
:config_options: slide_number=true controls=false
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
text = Text("Hello")
self.wipe([], text)
self.next_slide()
self.play(text.animate.scale(2))
self.next_slide()
self.zoom(text)
""" # noqa: D400, D415
from __future__ import annotations
import csv
import itertools as it
import re
import shlex
import sys
from pathlib import Path
from timeit import timeit
@ -195,6 +257,10 @@ class ManimSlidesDirective(Directive):
"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"),
"template": lambda arg: Path(arg),
"config_options": lambda arg: dict(
option.split("=") for option in shlex.split(arg)
),
}
final_argument_whitespace = True
@ -333,9 +399,20 @@ class ManimSlidesDirective(Directive):
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")
)
RevealJS(presentation_configs=presentation_configs, controls="true").convert_to(
destfile
)
template = self.options.get("template", None)
if template:
template = source_file_name.parents[0].joinpath(template)
config_options = self.options.get("config_options", {})
config_options.setdefault("controls", "true")
RevealJS(
presentation_configs=presentation_configs,
template=template,
**config_options,
).convert_to(destfile)
rendered_template = jinja2.Template(TEMPLATE).render(
clsname=clsname,

View File

@ -16,12 +16,14 @@ Utilities for using Manim Slides with IPython (in particular: Jupyter notebooks)
This magic requires two additional dependencies: ``manim`` and ``IPython``.
You can install them manually, or with the extra keyword:
pip install manim-slides[magic]
.. code-block:: bash
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>`_.
""" # noqa: D400, D415
"""
from __future__ import annotations
@ -142,6 +144,7 @@ class ManimSlidesMagic(Magics): # type: ignore
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 local_ns is None:
local_ns = {}

View File

@ -41,6 +41,9 @@ def make_logger() -> logging.Logger:
logger.setLevel(logging.getLogger("manim").level)
logger.addHandler(rich_handler)
if not (libav_logger := logging.getLogger("libav")).hasHandlers():
libav_logger.addHandler(rich_handler)
return logger

View File

@ -6,18 +6,27 @@ from typing import List, Optional, Tuple
import click
from click import Context, Parameter
from pydantic import ValidationError
from PySide6.QtCore import Qt
from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
from ..qt_utils import qapp
from .player import Player
ASPECT_RATIO_MODES = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
PREFERRED_QT_VERSIONS = ("6.5.1", "6.5.2")
def warn_if_non_desirable_pyside6_version() -> None:
from qtpy import API, QT_VERSION
if sys.version_info < (3, 12) and (
API != "pyside6" or QT_VERSION not in PREFERRED_QT_VERSIONS
):
logger.warn(
f"You are using {API = }, {QT_VERSION = }, "
"but we recommend installing 'PySide6==6.5.2', mainly to avoid "
"flashing screens between slides, "
"see issue https://github.com/jeertmans/manim-slides/issues/293. "
"You can do so with `pip install 'manim-slides[pyside6]'`."
)
@click.command()
@ -130,7 +139,8 @@ def start_at_callback(
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 2 arguments are expected but you gave {n_values}, please use commas to separate them",
f"exactly 2 arguments are expected but you gave {n_values}, "
"please use commas to separate them",
ctx=ctx,
param=param,
)
@ -154,7 +164,7 @@ def start_at_callback(
"--skip-all",
is_flag=True,
help="Skip all slides, useful the test if slides are working. "
"Automatically sets `--exit-after-last-slide` to True.",
"Automatically sets ``--exit-after-last-slide`` to True.",
)
@click.option(
"--exit-after-last-slide",
@ -182,7 +192,7 @@ def start_at_callback(
type=str,
callback=start_at_callback,
default=(None, None),
help="Start presenting at (x, y), equivalent to --sacn x --sasn y, "
help="Start presenting at (x, y), equivalent to ``--sacn x --sasn y``, "
"and overrides values if not None.",
)
@click.option(
@ -217,7 +227,9 @@ def start_at_callback(
metavar="RATE",
type=float,
default=1.0,
help="Playback rate of the video slides, see PySide6 docs for details.",
help="Playback rate of the video slides, see PySide6 docs for details. "
" The playback rate of each slide is defined as the product of its default "
" playback rate and the provided value.",
)
@click.option(
"--next-terminates-loop",
@ -225,6 +237,11 @@ def start_at_callback(
is_flag=True,
help="If set, pressing next will turn any looping slide into a play slide.",
)
@click.option(
"--hide-info-window",
is_flag=True,
help="Hide info window.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
@ -243,6 +260,7 @@ def present(
screen_number: Optional[int],
playback_rate: float,
next_terminates_loop: bool,
hide_info_window: bool,
) -> None:
"""
Present SCENE(s), one at a time, in order.
@ -275,6 +293,10 @@ def present(
if start_at[1]:
start_at_slide_number = start_at[1]
warn_if_non_desirable_pyside6_version()
from ..qt_utils import qapp
app = qapp()
app.setApplicationName("Manim Slides")
@ -290,6 +312,15 @@ def present(
else:
screen = None
from qtpy.QtCore import Qt
aspect_ratio_modes = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
from .player import Player
player = Player(
config,
presentation_configs,
@ -298,12 +329,13 @@ def present(
skip_all=skip_all,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio_mode=ASPECT_RATIO_MODES[aspect_ratio],
aspect_ratio_mode=aspect_ratio_modes[aspect_ratio],
presentation_index=start_at_scene_number,
slide_index=start_at_slide_number,
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
)
player.show()

View File

@ -1,11 +1,18 @@
from datetime import datetime
from pathlib import Path
from typing import Any, List, Optional
from typing import List, Optional
from PySide6.QtCore import Qt, QUrl, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from PySide6.QtMultimedia import QMediaPlayer
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtWidgets import QDialog, QGridLayout, QLabel, QMainWindow
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from qtpy.QtMultimedia import QMediaPlayer
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
from ..config import Config, PresentationConfig, SlideConfig
from ..logger import logger
@ -14,25 +21,145 @@ from ..resources import * # noqa: F403
WINDOW_NAME = "Manim Slides"
class Info(QDialog): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
class Info(QWidget): # type: ignore[misc]
key_press_event: Signal = Signal(QKeyEvent)
close_event: Signal = Signal(QCloseEvent)
def __init__(
self,
*,
full_screen: bool,
aspect_ratio_mode: Qt.AspectRatioMode,
screen: Optional[QScreen],
) -> None:
super().__init__()
if screen:
self.setScreen(screen)
self.move(screen.geometry().topLeft())
if full_screen:
self.setWindowState(Qt.WindowFullScreen)
layout = QHBoxLayout()
# Current slide view
left_layout = QVBoxLayout()
left_layout.addWidget(
QLabel("Current slide"),
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
main_video_widget = QVideoWidget()
main_video_widget.setAspectRatioMode(aspect_ratio_mode)
main_video_widget.setFixedSize(720, 480)
self.video_sink = main_video_widget.videoSink()
left_layout.addWidget(main_video_widget)
# Current slide informations
layout = QGridLayout()
self.scene_label = QLabel()
self.slide_label = QLabel()
self.start_time = datetime.now()
self.time_label = QLabel()
self.elapsed_label = QLabel("00h00m00s")
self.timer = QTimer()
self.timer.start(1000) # every second
self.timer.timeout.connect(self.update_time)
layout.addWidget(QLabel("Scene:"), 1, 1)
layout.addWidget(QLabel("Slide:"), 2, 1)
layout.addWidget(self.scene_label, 1, 2)
layout.addWidget(self.slide_label, 2, 2)
self.setLayout(layout)
self.setFixedWidth(150)
self.setFixedHeight(80)
bottom_left_layout = QHBoxLayout()
bottom_left_layout.addWidget(
QLabel("Scene:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.scene_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Slide:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.slide_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Time:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.time_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Elapsed:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.elapsed_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
left_layout.addLayout(bottom_left_layout)
layout.addLayout(left_layout)
if parent := self.parent():
self.closeEvent = parent.closeEvent
self.keyPressEvent = parent.keyPressEvent
layout.addSpacing(20)
# Next slide preview
right_layout = QVBoxLayout()
right_layout.addWidget(
QLabel("Next slide"),
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
next_video_widget = QVideoWidget()
next_video_widget.setAspectRatioMode(aspect_ratio_mode)
next_video_widget.setFixedSize(360, 240)
self.next_media_player = QMediaPlayer()
self.next_media_player.setVideoOutput(next_video_widget)
self.next_media_player.setLoops(-1)
right_layout.addWidget(next_video_widget)
# Notes
self.slide_notes = QLabel()
self.slide_notes.setWordWrap(True)
self.slide_notes.setTextFormat(Qt.TextFormat.MarkdownText)
self.slide_notes.setFixedWidth(360)
right_layout.addWidget(
self.slide_notes,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
layout.addLayout(right_layout)
widget = QWidget()
widget.setLayout(layout)
main_layout = QVBoxLayout()
main_layout.addWidget(widget, alignment=Qt.AlignmentFlag.AlignCenter)
self.setLayout(main_layout)
@Slot()
def update_time(self) -> None:
now = datetime.now()
seconds = (now - self.start_time).total_seconds()
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
self.time_label.setText(now.strftime("%Y/%m/%d %H:%M:%S"))
self.elapsed_label.setText(
f"{int(hours):02d}h{int(minutes):02d}m{int(seconds):02d}s"
)
@Slot()
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close_event.emit(event)
@Slot()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
self.key_press_event.emit(event)
class Player(QMainWindow): # type: ignore[misc]
@ -55,6 +182,7 @@ class Player(QMainWindow): # type: ignore[misc]
screen: Optional[QScreen] = None,
playback_rate: float = 1.0,
next_terminates_loop: bool = False,
hide_info_window: bool = False,
):
super().__init__()
@ -98,17 +226,26 @@ class Player(QMainWindow): # type: ignore[misc]
self.setWindowIcon(self.icon)
self.video_widget = QVideoWidget()
self.video_sink = self.video_widget.videoSink()
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
self.setCentralWidget(self.video_widget)
self.media_player = QMediaPlayer(self)
self.media_player.setVideoOutput(self.video_widget)
self.media_player.setPlaybackRate(playback_rate)
self.playback_rate = playback_rate
self.presentation_changed.connect(self.presentation_changed_callback)
self.slide_changed.connect(self.slide_changed_callback)
self.info = Info(parent=self)
self.info = Info(
full_screen=full_screen, aspect_ratio_mode=aspect_ratio_mode, screen=screen
)
self.info.close_event.connect(self.closeEvent)
self.info.key_press_event.connect(self.keyPressEvent)
self.video_sink.videoFrameChanged.connect(
lambda frame: self.info.video_sink.setVideoFrame(frame)
)
self.hide_info_window = hide_info_window
# Connecting key callbacks
@ -134,7 +271,7 @@ class Player(QMainWindow): # type: ignore[misc]
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
self.media_player.setLoops(1) # Otherwise looping slides never end
if status == QMediaPlayer.EndOfMedia:
if status == QMediaPlayer.MediaStatus.EndOfMedia:
self.load_next_slide()
self.media_player.mediaStatusChanged.connect(media_status_changed)
@ -143,7 +280,7 @@ class Player(QMainWindow): # type: ignore[misc]
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
if (
status == QMediaPlayer.EndOfMedia
status == QMediaPlayer.MediaStatus.EndOfMedia
and self.current_slide_config.auto_next
):
self.load_next_slide()
@ -218,6 +355,28 @@ class Player(QMainWindow): # type: ignore[misc]
def current_file(self, file: Path) -> None:
self.__current_file = file
@property
def next_slide_config(self) -> Optional[SlideConfig]:
if self.playing_reversed_slide:
return self.current_slide_config
elif self.current_slide_index < self.current_slides_count - 1:
return self.presentation_configs[self.current_presentation_index].slides[
self.current_slide_index + 1
]
elif self.current_presentation_index < self.presentations_count - 1:
return self.presentation_configs[
self.current_presentation_index + 1
].slides[0]
else:
return None
@property
def next_file(self) -> Optional[Path]:
if slide_config := self.next_slide_config:
return slide_config.file # type: ignore[no-any-return]
return None
@property
def playing_reversed_slide(self) -> bool:
return self.__playing_reversed_slide
@ -231,9 +390,18 @@ class Player(QMainWindow): # type: ignore[misc]
"""
def load_current_media(self, start_paused: bool = False) -> None:
url = QUrl.fromLocalFile(self.current_file)
url = QUrl.fromLocalFile(str(self.current_file))
self.media_player.setSource(url)
if self.playing_reversed_slide:
self.media_player.setPlaybackRate(
self.current_slide_config.reversed_playback_rate * self.playback_rate
)
else:
self.media_player.setPlaybackRate(
self.current_slide_config.playback_rate * self.playback_rate
)
if start_paused:
self.media_player.pause()
else:
@ -267,6 +435,7 @@ class Player(QMainWindow): # type: ignore[misc]
def load_next_slide(self) -> None:
if self.playing_reversed_slide:
self.playing_reversed_slide = False
self.preview_next_slide() # Slide number did not change, but next did
elif self.current_slide_index < self.current_slides_count - 1:
self.current_slide_index += 1
elif self.current_presentation_index < self.presentations_count - 1:
@ -301,19 +470,30 @@ class Player(QMainWindow): # type: ignore[misc]
index = self.current_slide_index
count = self.current_slides_count
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.slide_notes.setText(self.current_slide_config.notes)
self.preview_next_slide()
def preview_next_slide(self) -> None:
if slide_config := self.next_slide_config:
url = QUrl.fromLocalFile(str(slide_config.file))
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()
def show(self) -> None:
super().show()
self.info.show()
if not self.hide_info_window:
self.info.show()
@Slot()
def close(self) -> None:
logger.info("Closing gracefully...")
self.info.close()
super().close()
@Slot()
def next(self) -> None:
if self.media_player.playbackState() == QMediaPlayer.PausedState:
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif self.next_terminates_loop and self.media_player.loops() != 1:
position = self.media_player.position()
@ -331,6 +511,7 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def reverse(self) -> None:
self.load_reversed_slide()
self.preview_next_slide()
@Slot()
def replay(self) -> None:
@ -340,9 +521,9 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def play_pause(self) -> None:
state = self.media_player.playbackState()
if state == QMediaPlayer.PausedState:
if state == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif state == QMediaPlayer.PlayingState:
elif state == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
@Slot()

View File

@ -1,6 +1,6 @@
"""Qt utils."""
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
def qapp() -> QApplication:

54
manim_slides/render.py Normal file
View File

@ -0,0 +1,54 @@
"""
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
1. You can are sure to execute the rendering command with the same Python environment
as for ``manim-slides``.
2. You can pass options to the config.
"""
import subprocess
import sys
from typing import Tuple
import click
@click.command(
context_settings=dict(
ignore_unknown_options=True, allow_extra_args=True, help_option_names=("-h",)
),
options_metavar="[-h] [--CE|--GL]",
)
@click.option(
"--CE",
is_flag=True,
envvar="MANIM_RENDERER",
show_envvar=True,
help="If set, use Manim Community Edition (CE) renderer. "
"If this or ``--GL`` is not set, defaults to CE renderer.",
)
@click.option(
"--GL",
is_flag=True,
envvar="MANIMGL_RENDERER",
show_envvar=True,
help="If set, use ManimGL renderer.",
)
@click.argument("args", metavar="[RENDERER_ARGS]...", nargs=-1, type=click.UNPROCESSED)
def render(ce: bool, gl: bool, args: Tuple[str, ...]) -> None:
"""
Render SCENE(s) from the input FILE, using the specified renderer.
Use ``manim-slides render --help`` to see help information for
a specific renderer.
"""
if ce and gl:
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
if gl:
subprocess.run([sys.executable, "-m", "manimlib", *args])
else:
subprocess.run([sys.executable, "-m", "manim", "render", *args])

View File

@ -4,7 +4,7 @@
# Created by: The Resource Compiler for Qt version 6.4.0
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
from qtpy import QtCore
qt_resource_data = b"\
\x00\x00\x08\x1c\

View File

@ -58,6 +58,7 @@ class Wipe(AnimationGroup): # type: ignore[misc]
self.next_slide()
self.play(Wipe(circle, square, shift=3 * LEFT))
"""
def __init__(
@ -122,6 +123,7 @@ class Zoom(AnimationGroup): # type: ignore[misc]
for i in range(2):
self.play(Zoom(circles[i], circles[i+1]))
self.next_slide()
"""
def __init__(

View File

@ -1,19 +1,30 @@
from __future__ import annotations
__all__ = ["BaseSlide"]
import platform
from abc import abstractmethod
from pathlib import Path
from typing import Any, List, MutableMapping, Optional, Sequence, Tuple, ValuesView
from typing import (
TYPE_CHECKING,
Any,
MutableMapping,
Sequence,
ValuesView,
)
import numpy as np
from tqdm import tqdm
from ..config import PresentationConfig, PreSlideConfig, SlideConfig
from ..defaults import FFMPEG_BIN, FOLDER_PATH
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
from ..defaults import FOLDER_PATH
from ..logger import logger
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
from . import MANIM
if TYPE_CHECKING:
from .animation import Wipe, Zoom
if MANIM:
from manim.mobject.mobject import Mobject
else:
@ -28,19 +39,14 @@ class BaseSlide:
) -> None:
super().__init__(*args, **kwargs)
self._output_folder: Path = output_folder
self._slides: List[PreSlideConfig] = []
self._pre_slide_config_kwargs: MutableMapping[str, Any] = {}
self._slides: list[PreSlideConfig] = []
self._base_slide_config: BaseSlideConfig = BaseSlideConfig()
self._current_slide = 1
self._current_animation = 0
self._start_animation = 0
self._canvas: MutableMapping[str, Mobject] = {}
self._wait_time_between_slides = 0.0
@property
def _ffmpeg_bin(self) -> Path:
"""Return the path to the ffmpeg binaries."""
return FFMPEG_BIN
@property
@abstractmethod
def _frame_height(self) -> float:
@ -61,13 +67,13 @@ class BaseSlide:
@property
@abstractmethod
def _resolution(self) -> Tuple[int, int]:
def _resolution(self) -> tuple[int, int]:
"""Return the scene's resolution used during rendering."""
raise NotImplementedError
@property
@abstractmethod
def _partial_movie_files(self) -> List[Path]:
def _partial_movie_files(self) -> list[Path]:
"""Return a list of partial movie files, a.k.a animations."""
raise NotImplementedError
@ -85,7 +91,7 @@ class BaseSlide:
@property
@abstractmethod
def _start_at_animation_number(self) -> Optional[int]:
def _start_at_animation_number(self) -> int | None:
"""If set, return the animation number at which rendering start."""
raise NotImplementedError
@ -150,6 +156,7 @@ class BaseSlide:
self.remove_from_canvas("title", "slide_number")
self.wipe(self.mobjects_without_canvas, [])
"""
return self._canvas
@ -185,7 +192,9 @@ class BaseSlide:
the canvas.
"""
return [
mobject for mobject in self.mobjects if mobject not in self.canvas_mobjects # type: ignore[attr-defined]
mobject
for mobject in self.mobjects # type: ignore[attr-defined]
if mobject not in self.canvas_mobjects
]
@property
@ -240,6 +249,7 @@ class BaseSlide:
self.next_slide()
self.play(FadeOut(circle))
"""
return self._wait_time_between_slides
@ -252,8 +262,12 @@ class BaseSlide:
super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1
@BaseSlideConfig.wrapper("base_slide_config")
def next_slide(
self, *, loop: bool = False, auto_next: bool = False, **kwargs: Any
self,
*,
base_slide_config: BaseSlideConfig,
**kwargs: Any,
) -> None:
"""
Create a new slide with previous animations, and setup options
@ -274,6 +288,23 @@ class BaseSlide:
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
:param playback_rate:
Playback rate at which the video is played.
Note that this is only supported by ``manim-slides present``.
:param reversed_playback_rate:
Playback rate at which the reversed video is played.
Note that this is only supported by ``manim-slides present``.
:param notes:
Presenter notes, in Markdown format.
Note that PowerPoint does not support Markdown.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html/pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param kwargs:
Keyword arguments to be passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
@ -358,22 +389,46 @@ class BaseSlide:
self.next_slide()
self.wipe(square)
The following contains speaker notes. On the webbrowser,
the speaker view can be triggered by pressing :kbd:`S`.
.. manim-slides:: SpeakerNotesExample
from manim import *
from manim_slides import Slide
class SpeakerNotesExample(Slide):
def construct(self):
self.next_slide(notes="Some introduction")
square = Square(color=GREEN, side_length=2)
self.play(GrowFromCenter(square))
self.next_slide(notes="We now rotate the slide")
self.play(Rotate(square, PI / 2))
self.next_slide(notes="Bye bye")
self.zoom(square)
"""
if self._current_animation > self._start_animation:
if self.wait_time_between_slides > 0.0:
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
self._slides.append(
PreSlideConfig(
start_animation=self._start_animation,
end_animation=self._current_animation,
**self._pre_slide_config_kwargs,
PreSlideConfig.from_base_slide_config_and_animation_indices(
self._base_slide_config,
self._start_animation,
self._current_animation,
)
)
self._current_slide += 1
self._pre_slide_config_kwargs = dict(loop=loop, auto_next=auto_next)
self._base_slide_config = base_slide_config
self._start_animation = self._current_animation
def _add_last_slide(self) -> None:
@ -385,10 +440,10 @@ class BaseSlide:
return
self._slides.append(
PreSlideConfig(
start_animation=self._start_animation,
end_animation=self._current_animation,
**self._pre_slide_config_kwargs,
PreSlideConfig.from_base_slide_config_and_animation_indices(
self._base_slide_config,
self._start_animation,
self._current_animation,
)
)
@ -407,7 +462,7 @@ class BaseSlide:
scene_files_folder.mkdir(parents=True, exist_ok=True)
files: List[Path] = self._partial_movie_files
files: list[Path] = self._partial_movie_files
# We must filter slides that end before the animation offset
if offset := self._start_at_animation_number:
@ -418,7 +473,7 @@ class BaseSlide:
slide.start_animation = max(0, slide.start_animation - offset)
slide.end_animation -= offset
slides: List[SlideConfig] = []
slides: list[SlideConfig] = []
for pre_slide_config in tqdm(
self._slides,
@ -435,11 +490,11 @@ class BaseSlide:
# We only concat animations if it was not present
if not use_cache or not dst_file.exists():
concatenate_video_files(self._ffmpeg_bin, slide_files, dst_file)
concatenate_video_files(slide_files, dst_file)
# We only reverse video if it was not present
if not use_cache or not rev_file.exists():
reverse_video_file(self._ffmpeg_bin, dst_file, rev_file)
reverse_video_file(dst_file, rev_file)
slides.append(
SlideConfig.from_pre_slide_config_and_files(
@ -467,8 +522,9 @@ class BaseSlide:
self,
*args: Any,
direction: np.ndarray = LEFT,
return_animation: bool = False,
**kwargs: Any,
) -> None:
) -> Wipe | None:
"""
Play a wipe animation that will shift all the current objects outside of the
current scene's scope, and all the future objects inside.
@ -476,6 +532,8 @@ class BaseSlide:
:param args: Positional arguments passed to
:class:`Wipe<manim_slides.slide.animation.Wipe>`.
:param direction: The wipe direction, that will be scaled by the scene size.
:param return_animation: If set, return the animation instead of
playing it. This is useful to combine multiple animations with this one.
:param kwargs: Keyword arguments passed to
:class:`Wipe<manim_slides.slide.animation.Wipe>`.
@ -502,7 +560,14 @@ class BaseSlide:
self.wipe(Group(square, text), beautiful, direction=UP)
self.next_slide()
self.wipe(beautiful, circle, direction=DOWN + RIGHT)
anim = self.wipe(
beautiful,
circle,
direction=DOWN + RIGHT,
return_animation=True
)
self.play(anim)
"""
from .animation import Wipe
@ -517,13 +582,18 @@ class BaseSlide:
**kwargs,
)
if return_animation:
return animation
self.play(animation)
return None
def zoom(
self,
*args: Any,
return_animation: bool = False,
**kwargs: Any,
) -> None:
) -> Zoom | None:
"""
Play 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
@ -531,6 +601,8 @@ class BaseSlide:
:param args: Positional arguments passed to
:class:`Zoom<manim_slides.slide.animation.Zoom>`.
:param return_animation: If set, return the animation instead of
playing it. This is useful to combine multiple animations with this one.
:param kwargs: Keyword arguments passed to
:class:`Zoom<manim_slides.slide.animation.Zoom>`.
@ -552,10 +624,22 @@ class BaseSlide:
self.zoom(circle, square)
self.next_slide()
self.zoom(square, circle, out=True, scale=10.0)
anim = self.zoom(
square,
circle,
out=True,
scale=10.0,
return_animation=True
)
self.play(anim)
"""
from .animation import Zoom
animation = Zoom(*args, **kwargs)
if return_animation:
return animation
self.play(animation)
return None

View File

@ -3,6 +3,7 @@ from typing import Any, List, Optional, Tuple
from manim import Scene, ThreeDScene, config
from ..config import BaseSlideConfig
from .base import BaseSlide
@ -12,15 +13,6 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
for slides rendering.
"""
@property
def _ffmpeg_bin(self) -> Path:
# Prior to v0.16.0.post0,
# ffmpeg was stored as a constant in manim.constants
try:
return Path(config.ffmpeg_executable)
except AttributeError:
return super()._ffmpeg_bin
@property
def _frame_height(self) -> float:
return config["frame_height"] # type: ignore
@ -79,11 +71,18 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
"""
self.next_slide(*args, **kwargs)
@BaseSlideConfig.wrapper("base_slide_config")
def next_slide(
self, *args: Any, loop: bool = False, auto_next: bool = False, **kwargs: Any
self,
*args: Any,
base_slide_config: BaseSlideConfig,
**kwargs: Any,
) -> None:
Scene.next_section(self, *args, **kwargs)
BaseSlide.next_slide(self, loop=loop, auto_next=auto_next)
BaseSlide.next_slide.__wrapped__(
self,
base_slide_config=base_slide_config,
)
def render(self, *args: Any, **kwargs: Any) -> None:
"""MANIM render."""
@ -145,6 +144,7 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
self.next_slide()
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
"""
pass

View File

@ -40,6 +40,9 @@
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}>
{% if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
{%- endfor -%}
{%- endfor -%}
@ -50,9 +53,17 @@
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}
<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealMarkdown, RevealNotes ],
{%- endif %}
// 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.
@ -305,28 +316,36 @@
hideCursorTime: {{ hide_cursor_time }}
});
{% if data_uri %}
// 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');
{% if data_uri -%}
// 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}`);
}
});
}
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 );
{% endif %}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{%- endif %}
</script>
{% if env['READTHEDOCS'] -%}
<style>
readthedocs-flyout, readthedocs-notification {
display: none;
}
</style>
{%- endif %}
</body>
</html>

View File

@ -1,45 +1,37 @@
import hashlib
import subprocess
import tempfile
from pathlib import Path
from typing import List
import av
from .logger import logger
def concatenate_video_files(ffmpeg_bin: Path, files: List[Path], dest: Path) -> None:
def concatenate_video_files(files: List[Path], dest: Path) -> None:
"""Concatenate multiple video files into one."""
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
f.writelines(f"file '{path.absolute()}'\n" for path in files)
f.close()
command: List[str] = [
str(ffmpeg_bin),
"-f",
"concat",
"-safe",
"0",
"-i",
f.name,
"-c",
"copy",
str(dest),
"-y",
]
logger.debug(" ".join(command))
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
input_ = av.open(f.name, options={"safe": "0"}, format="concat")
input_stream = input_.streams.video[0]
output = av.open(str(dest), mode="w")
output_stream = output.add_stream(
template=input_stream,
)
if output:
logger.debug(output.decode())
for packet in input_.demux(input_stream):
# We need to skip the "flushing" packets that `demux` generates.
if packet.dts is None:
continue
if error:
logger.debug(error.decode())
# We need to assign the packet to the new stream.
packet.stream = output_stream
output.mux(packet)
if not dest.exists():
raise ValueError(
"could not properly concatenate files, use `-v DEBUG` for more details"
)
input_.close()
output.close()
def merge_basenames(files: List[Path]) -> Path:
@ -63,15 +55,44 @@ def merge_basenames(files: List[Path]) -> Path:
return dirname.joinpath(basename + ext)
def reverse_video_file(ffmpeg_bin: Path, src: Path, dst: Path) -> None:
"""Reverses a video file, writting the result to `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()
def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
"""Code from https://github.com/PyAV-Org/PyAV/issues/239."""
for c, n in zip(nodes, nodes[1:]):
c.link_to(n)
if output:
logger.debug(output.decode())
if error:
logger.debug(error.decode())
def reverse_video_file(src: Path, dest: Path) -> None:
"""Reverses a video file, writting the result to `dest`."""
input_ = av.open(str(src))
input_stream = input_.streams.video[0]
output = av.open(str(dest), mode="w")
output_stream = output.add_stream(codec_name="libx264", rate=input_stream.base_rate)
output_stream.width = input_stream.width
output_stream.height = input_stream.height
output_stream.pix_fmt = input_stream.pix_fmt
graph = av.filter.Graph()
link_nodes(
graph.add_buffer(template=input_stream),
graph.add("reverse"),
graph.add("buffersink"),
)
graph.configure()
frames_count = 0
for frame in input_.decode(video=0):
graph.push(frame)
frames_count += 1
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
for _ in range(frames_count):
frame = graph.pull()
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
output.mux(output_stream.encode(frame))
for packet in output_stream.encode():
output.mux(packet)
input_.close()
output.close()

View File

@ -0,0 +1,85 @@
import sys
from pathlib import Path
import click
from ..commons import config_options, verbosity_option
from ..config import Config
from ..defaults import CONFIG_PATH
from ..logger import logger
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: 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: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""
Actual initialization code for configuration file, with optional interactive
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
choice = click.prompt(
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
type=click.Choice(["o", "m", "q"], case_sensitive=False),
)
force = choice == "o"
merge = choice == "m"
if not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
config = Config.from_file(config_path)
from ..qt_utils import qapp
from .wizard import Wizard
app = qapp()
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
if window.closed_without_saving:
sys.exit(0)
config = window.config
if merge:
config = Config.from_file(config_path).merge_with(config)
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")

View File

@ -1,12 +1,9 @@
import sys
from functools import partial
from pathlib import Path
from typing import Any
import click
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QKeyEvent
from PySide6.QtWidgets import (
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon, QKeyEvent
from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
QGridLayout,
@ -17,12 +14,9 @@ from PySide6.QtWidgets import (
QWidget,
)
from .commons import config_options, verbosity_option
from .config import Config, Key
from .defaults import CONFIG_PATH
from .logger import logger
from .qt_utils import qapp
from .resources import * # noqa: F403
from ..config import Config, Key
from ..logger import logger
from ..resources import * # noqa: F403
WINDOW_NAME: str = "Configuration Wizard"
@ -125,76 +119,3 @@ class Wizard(QWidget): # type: ignore
key_name = keymap[dialog.key]
key.set_ids(dialog.key)
button.setText(key_name)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: 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: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""
Actual initialization code for configuration file, with optional interactive
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
choice = click.prompt(
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
type=click.Choice(["o", "m", "q"], case_sensitive=False),
)
force = choice == "o"
merge = choice == "m"
if not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
config = Config.from_file(config_path)
app = qapp()
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
if window.closed_without_saving:
sys.exit(0)
config = window.config
if merge:
config = Config.from_file(config_path).merge_with(config)
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")

3453
pdm.lock generated Normal file

File diff suppressed because it is too large Load Diff

4239
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,90 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["setuptools", "poetry-core>=1.0.0"]
build-backend = "pdm.backend"
requires = ["pdm-backend", "setuptools"]
[tool.black]
target-version = ["py38"]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering",
]
dependencies = [
"av>=9.0.0",
"click>=8.1.3",
"click-default-group>=1.2.2",
"jinja2>=3.1.2",
"lxml>=4.9.2",
"numpy>=1.19",
"opencv-python>=4.6.0.66",
"pillow>=9.5.0",
"pydantic>=2.0.1",
"pydantic-extra-types>=2.0.0",
"python-pptx>=0.6.21",
"qtpy>=2.4.1",
"requests>=2.28.1",
"rich>=13.3.2",
"rtoml>=0.9.0",
"tqdm>=4.64.1",
]
description = "Tool for live presentations using manim"
dynamic = ["version"]
keywords = ["manim", "slides", "plugin", "manimgl"]
license = {text = "MIT"}
name = "manim-slides"
readme = "README.md"
requires-python = ">=3.9,<3.13"
[project.optional-dependencies]
docs = [
"manim-slides[magic,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"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",
]
full = [
"manim-slides[magic,manim,manimgl,sphinx-directive]",
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.6.1"]
pyqt6 = ["pyqt6>=6.6.1"]
pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
[project.scripts]
manim-slides = "manim_slides.__main__:cli"
[project.urls]
Changelog = "https://github.com/jeertmans/manim-slides/releases"
Documentation = "https://eertmans.be/manim-slides"
Founding = "https://github.com/sponsors/jeertmans"
Homepage = "https://github.com/jeertmans/manim-slides"
Repository = "https://github.com/jeertmans/manim-slides"
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
precision = 2
[tool.mypy]
disallow_untyped_decorators = false
@ -11,118 +92,45 @@ install_types = true
python_version = "3.8"
strict = true
[tool.poetry]
authors = [
"Jérome Eertmans <jeertmans@icloud.com>"
[tool.pdm.dev-dependencies]
dev = [
"bump2version>=1.0.1",
"pre-commit>=3.5.0",
]
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 :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering"
github-action = ["setuptools"]
test = [
"manim-slides[manim,manimgl,pyqt6]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
"pytest-xdist>=3.3.1",
]
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 = "5.0.0"
[tool.poetry.dependencies]
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 = "^3.1.2"
lxml = "^4.9.2"
manim = {version = "^0.17.3", optional = true}
manimgl = {version = "^1.6.1", 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.2"
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.pdm.resolution.overrides]
manimpango = "<1.0.0,>=0.5.0" # This conflicts with ManimGL, hopefully not an issue
skia-pathops = "0.8.0.post1" # From manim 0.18.0 (Python 3.12 support)
[tool.poetry.extras]
magic = ["manim", "ipython"]
manim = ["manim"]
manimgl = ["manimgl"]
sphinx-directive = ["docutils", "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"
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-env = "^0.8.2"
pytest-qt = "^4.2.0"
pytest-xdist = "^3.3.1"
[tool.poetry.plugins]
[tool.poetry.plugins."console_scripts"]
manim-slides = "manim_slides.__main__:cli"
[tool.pdm.version]
path = "manim_slides/__version__.py"
source = "file"
[tool.pytest.ini_options]
env = [
"QT_QPA_PLATFORM=offscreen"
"QT_QPA_PLATFORM=offscreen",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning"
"ignore::DeprecationWarning",
]
[tool.ruff]
extend-exclude = ["manim_slides/resources.py"]
extend-include = ["*.ipynb"]
line-length = 88
target-version = "py38"
[tool.ruff.lint]
extend-ignore = [
"D100",
"D101",
@ -135,9 +143,7 @@ extend-ignore = [
"D203",
"D205",
"D212",
"E501"
"E501",
]
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ['manim_slides', 'tests']}
line-length = 88
target-version = "py38"
isort = {known-first-party = ["manim_slides", "tests"]}

View File

@ -1,21 +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
MANIM_SLIDES_THEME=light pdm run manim-slides 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
MANIM_SLIDES_THEME=dark_docs pdm run manim-slides 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
MANIM_SLIDES_THEME=dark_github pdm run manim-slides 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
MANIM_SLIDES_THEME=light pdm run manim-slides 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
MANIM_SLIDES_THEME=dark_docs pdm run manim-slides 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

View File

@ -2,7 +2,6 @@ from typing import MutableMapping
import pytest
from manim_slides.defaults import FFMPEG_BIN
from manim_slides.slide.base import BaseSlide
@ -12,9 +11,6 @@ def base_slide() -> BaseSlide:
class TestBaseSlide:
def test_ffmpeg_bin(self, base_slide: BaseSlide) -> None:
assert base_slide._ffmpeg_bin == FFMPEG_BIN
def test_frame_height(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._frame_height

View File

@ -1,7 +1,6 @@
import shutil
from pathlib import Path
from manim_slides.defaults import CONFIG_PATH, FFMPEG_BIN, FOLDER_PATH
from manim_slides.defaults import CONFIG_PATH, FOLDER_PATH
def test_folder_path() -> None:
@ -10,13 +9,3 @@ def test_folder_path() -> None:
def test_config_path() -> None:
assert CONFIG_PATH == Path(".manim-slides.toml")
def test_ffmpeg_bin() -> None:
assert FFMPEG_BIN == Path("ffmpeg")
def test_ffmpeg_bin_exists() -> None:
assert (
shutil.which(str(FFMPEG_BIN)) is not None
), "If this fails, many other tests will fail"

View File

@ -64,6 +64,28 @@ def test_convert(slides_folder: Path, extension: str) -> None:
assert results.exit_code == 0
@pytest.mark.parametrize(
("extension", "expected_log"),
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],
)
def test_convert_auto(slides_folder: Path, extension: str, expected_log: str) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"convert",
"BasicSlide",
f"basic_example.{extension}",
"--folder",
str(slides_folder),
],
)
assert results.exit_code == 0, expected_log in results.output
def test_init() -> None:
runner = CliRunner()

View File

@ -3,7 +3,7 @@ from typing import Iterator, Tuple
import pytest
from click.testing import CliRunner
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
from manim_slides.present import present
@ -11,12 +11,12 @@ from manim_slides.present import present
@pytest.fixture(autouse=True)
def auto_shutdown_qapp() -> Iterator[None]:
if app := QApplication.instance():
app.shutdown()
app.quit()
yield
if app := QApplication.instance():
app.shutdown()
app.quit()
@pytest.fixture(scope="session")

View File

@ -1,4 +1,4 @@
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
from manim_slides.qt_utils import qapp

View File

@ -1,10 +1,8 @@
import random
import shutil
import subprocess
import sys
from pathlib import Path
import click
import numpy as np
import pytest
from click.testing import CliRunner
from manim import (
@ -20,37 +18,30 @@ from manim import (
GrowFromCenter,
Text,
)
from manim.__main__ import main as manim_cli
from packaging import version
from pydantic import ValidationError
from manim_slides.config import PresentationConfig
from manim_slides.defaults import FOLDER_PATH
from manim_slides.render import render
from manim_slides.slide.manim import Slide
@click.command(
context_settings=dict(
ignore_unknown_options=True,
allow_extra_args=True,
)
)
@click.pass_context
def manimgl_cli(ctx: click.Context) -> None:
subprocess.run([sys.executable, "-m", "manimlib", *ctx.args])
cli = pytest.mark.parametrize(
["cli"],
@pytest.mark.parametrize(
"renderer",
[
[manim_cli],
[manimgl_cli],
"--CE",
pytest.param(
"--GL",
marks=pytest.mark.skipif(
version.parse(np.__version__) >= version.parse("1.25"),
reason="ManimGL requires numpy<1.25, which is outdate",
),
),
],
)
@cli
def test_render_basic_slide(
cli: click.Command,
renderer: str,
slides_file: Path,
presentation_config: PresentationConfig,
manimgl_config: Path,
@ -59,9 +50,11 @@ def test_render_basic_slide(
with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke(cli, [str(slides_file), "BasicSlide", "-ql"])
results = runner.invoke(
render, [renderer, str(slides_file), "BasicSlide", "-ql"]
)
assert results.exit_code == 0
assert results.exit_code == 0, results
local_slides_folder = (Path(tmp_dir) / "slides").resolve(strict=True)
@ -155,16 +148,16 @@ class TestSlide:
self.add(text)
assert "loop" not in self._pre_slide_config_kwargs
assert not self._base_slide_config.loop
self.next_slide(loop=True)
self.play(text.animate.scale(2))
assert self._pre_slide_config_kwargs["loop"]
assert self._base_slide_config.loop
self.next_slide(loop=False)
assert not self._pre_slide_config_kwargs["loop"]
assert not self._base_slide_config.loop
@assert_constructs
class TestAutoNext(Slide):
@ -173,16 +166,16 @@ class TestSlide:
self.add(text)
assert "auto_next" not in self._pre_slide_config_kwargs
assert not self._base_slide_config.auto_next
self.next_slide(auto_next=True)
self.play(text.animate.scale(2))
assert self._pre_slide_config_kwargs["auto_next"]
assert self._base_slide_config.auto_next
self.next_slide(auto_next=False)
assert not self._pre_slide_config_kwargs["auto_next"]
assert not self._base_slide_config.auto_next
@assert_constructs
class TestLoopAndAutoNextFails(Slide):
@ -197,6 +190,48 @@ class TestSlide:
with pytest.raises(ValidationError):
self.next_slide()
@assert_constructs
class TestPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert self._base_slide_config.playback_rate == 1.0
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
assert self._base_slide_config.playback_rate == 2.0
@assert_constructs
class TestReversedPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert self._base_slide_config.reversed_playback_rate == 1.0
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
assert self._base_slide_config.reversed_playback_rate == 2.0
@assert_constructs
class TestNotes(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert self._base_slide_config.notes == ""
self.next_slide(notes="test")
self.play(text.animate.scale(2))
assert self._base_slide_config.notes == "test"
@assert_constructs
class TestWipe(Slide):
def construct(self) -> None:

View File

@ -1,17 +1,18 @@
from pathlib import Path
from click.testing import CliRunner
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QApplication,
QMessageBox,
)
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from manim_slides.config import Config, Key
from manim_slides.defaults import CONFIG_PATH
from manim_slides.wizard import KeyInput, Wizard, init, wizard
from manim_slides.wizard import init, wizard
from manim_slides.wizard.wizard import KeyInput, Wizard
class TestKeyInput: