Compare commits

...

97 Commits

Author SHA1 Message Date
941b895083 chore(deps): bump version from 5.4.0 to 5.4.1 2025-01-22 21:12:20 +01:00
289b7c1683 fix(ci): lockfile 2025-01-22 21:12:14 +01:00
b07a83898b feat(lib): add start_skip_animations and stop_skip_animations meth. (#523) 2025-01-22 21:10:37 +01:00
074a029759 chore(deps): bump version from 5.3.1 to 5.4.0 2025-01-21 13:41:51 +01:00
b4af76050e fix(ci): lock 2025-01-21 13:41:43 +01:00
adce58e1b7 fix(lib): bump Manim and fix OpenGL renderer (#522)
* fix(lib): bump Manim and fix OpenGL renderer

* fix(tests): correctly change cwd
2025-01-21 13:38:41 +01:00
32ab690932 feat(lib): add skip_animations compatibility (#516)
* feat: Add skip_animations compatibility

* Add tests, config and changelog

* chore(fmt): auto fixes from pre-commit.com hooks

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

* Update manim_slides/slide/base.py

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

* chore(fmt): auto fixes from pre-commit.com hooks

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

* chore(tests): implement tests

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-21 11:27:21 +01:00
df31345f83 chore(deps): pre-commit autoupdate (#521)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.1 → v0.9.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.1...v0.9.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-21 10:11:13 +01:00
ef282300f1 chore(deps): bump version from 5.3.0 to 5.3.1 2025-01-15 14:57:28 +01:00
a9ba1b4fad fix(docker): try to fix Docker image 2025-01-15 14:56:39 +01:00
4cc6c2865d chore(deps): pre-commit autoupdate (#509)
* chore(deps): pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.9.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.9.1)
- [github.com/pre-commit/mirrors-mypy: v1.14.0 → v1.14.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.0...v1.14.1)

* chore(fmt): auto fixes from pre-commit.com hooks

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2025-01-15 14:38:37 +01:00
0483e2f861 chore(ci): fix RTD build (#518)
See https://github.com/ManimCommunity/manim/issues/4104
2025-01-15 14:30:31 +01:00
a58ff6c388 fix(convert): fix missing slides in HTML presentation after #508 (#515)
* fix(convert): fix missing slides after #508

* Update template.html

* Update CHANGELOG.md

* Update CHANGELOG.md

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

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2025-01-15 11:25:18 +01:00
bf512f2f73 fix(ci): typo in bug.yml issue template
See https://github.com/jeertmans/manim-slides/issues/513#issuecomment-2586771845
2025-01-15 10:17:56 +01:00
541b175360 chore(ci): fix bug issue template 2025-01-13 11:29:26 +01:00
57ab592d36 chore(deps): bump version from 5.2.0 to 5.3.0 2025-01-10 11:57:16 +01:00
840d1d80d9 fix(convert): avoid micro-stuttering with --one-file in HTML presentation (#508)
* fix: Fix micro-stuttering with --one-file in HTML presentation

* Update template.html

* Update CHANGELOG.md

* Update CHANGELOG.md

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

* Update CHANGELOG.md

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2025-01-09 14:13:57 +01:00
c15cd95565 chore(docs): Manim Slides survey 2025-01-08 11:34:37 +01:00
478552c528 fix(docs): dead links (#507) 2025-01-03 14:20:05 +01:00
1189f37cf3 feat(convert/html): inline CSS and JS with convert --one_file --offline (#505)
* feat: Inline CSS and JS by default with --offline

* chore(fmt): auto fixes from pre-commit.com hooks

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

* chore: Add test

* Add one_file parameter

* chore(fmt): auto fixes from pre-commit.com hooks

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

* Fix lint

* Fix typo

* Fix typo

* Fix IPython magic doc

* Update manim_slides/convert.py

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

* Add test for one_file=true

* chore(fmt): auto fixes from pre-commit.com hooks

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

* Update manim_slides/convert.py

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

* Update manim_slides/convert.py

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

* Update docs/source/reference/sharing.md

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

* Update manim_slides/convert.py

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

* chore(fmt): auto fixes from pre-commit.com hooks

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

* Add changelog and tests

* Fix IPython magic

* Update docs/source/faq.md

* Update CHANGELOG.md

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2025-01-03 13:49:05 +01:00
e50271b0b2 feat(cli): document config options (#485)
* feat(cli): document config options

* chpre(docs): add changelog entry

* fixes

* chore(deps): remove restriction

* fix(deps): PySide6 issue

See https://github.com/astral-sh/uv/issues/10056

* chore(docs): document config options

* chore(fmt): auto fixes from pre-commit.com hooks

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

* fox(docs): deps

* chore(tests): remove old test

* chore(fmt): 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>
2025-01-03 09:19:41 +01:00
98955bee5c fix(deps): bump ManimGL>=1.7.2 (#506)
* fix(deps): bump ManimGL>=1.7.2

This bumps and fixes issues related to latest ManimGL version, as it includes breaking changes.

* fix(docs): PR number

* fix(deps): PySide6 issue

See https://github.com/astral-sh/uv/issues/10056

* fix(ci): headers
2025-01-02 17:39:27 +01:00
2169938df7 chore(deps): bump version from 5.1.10 to 5.2.0 2024-12-24 11:12:02 +01:00
a207248deb chore(deps): update lock 2024-12-24 11:10:58 +01:00
1c859991b8 chore(deps): pre-commit autoupdate (#504)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)
- [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-24 11:10:04 +01:00
a24915665d chore(deps): pre-commit autoupdate (#501)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-18 10:44:08 +01:00
5cce117050 chore(deps): bump astral-sh/setup-uv from 3 to 4 (#492)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  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>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-11 16:10:01 +01:00
bd76fbdfd9 chore(deps): pre-commit autoupdate (#486)
* chore(deps): pre-commit autoupdate

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

* chore(fmt): 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-12-11 15:55:29 +01:00
5b026919ea chore(deps): bump ManimGL to 1.7.1 (#499)
* chore(deps): bump ManimGL to 1.7.1

Bump ManimGL's minimal version, so relax constraints on other deps and remove compatibility issues with Manim

* fix(docs): correct PR number

* fix(lib): update ManimGL's init

See https://github.com/3b1b/manim/issues/2261

* fix(lib): force float

* chore(tests): correctly ignore warning

* fix(tests)

* fix(tests): add skips

* chore(fmt): auto fixes from pre-commit.com hooks

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

* fix(tests)

* oops

* fix on 3.12

* fix(lib): correctly patch ManimGL

* fix(deps): pyrr issue

* fix: version

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-11 15:54:59 +01:00
05c1a16ca3 feat(cli): smarter info window hiding logic (#482)
* Adds features from #327

This changes the ``--hide-info-window-`` command to accept three options: auto, always, and never. Auto will hide the info window if there is only one screen. This also fixes the --full-screen option by moving the info window to monitor 1 and the presentation to monitor 0 by default.

* chore(fmt): auto fixes from pre-commit.com hooks

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

* Let Qt decide the screens

* Fix full screen bug

* chore(fmt): auto fixes from pre-commit.com hooks

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

* fix(cli): improve logic

* fix

* Revert fixes and clean code

* Update changelog

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-11 09:16:50 +01:00
3bd8c386b1 chore(deps): bump codecov/codecov-action from 4 to 5 (#491)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [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/v4...v5)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-06 18:05:18 +01:00
628c8da832 fix(convert): blank web page when converting multiple slides into HTML (#497)
* fix(convert): Blank web page when converting multiple slides into HTML

* chore(docs): add changelog

* Fix typo
2024-12-04 12:25:34 +01:00
3b62e6b788 chore(deps): bump version from 5.1.9 to 5.1.10 2024-12-03 12:57:36 +01:00
ff9aac49d7 chore(deps): pin av<14 to avoid breaking changes (#495)
* chore(deps): Pin `av<14` to avoid breaking changes

Closes #494

* chore(docs): syntax
2024-12-03 12:56:16 +01:00
16d9d32d33 fix(ci): actually run tests on multiple Python versions (#493) 2024-12-03 12:17:32 +01:00
988011ff7d feat(cli): allow multiple reverses (#488)
* Permit multiple reverses

* chore(docs): add changelog entry

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-11-12 12:09:35 +01:00
3dbe12b480 fix(ci): typo 2024-11-06 10:25:18 +01:00
6ba657c0d5 fix(ci): feature_request.yml 2024-11-06 10:24:50 +01:00
6c8ab61f9d chore(deps): pre-commit autoupdate (#480)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.7.1)
- [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.13.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-11-01 10:01:53 +01:00
91e6e139e3 fix(docs): some typos (#484) 2024-11-01 10:01:28 +01:00
d8acbae165 feat(convert): allow fully offline HTML presentation (#440)
* feat(cli): allow offline HTML presentations

* feat(convert): allow fully offline HTML presentation

TODO: check if this is really the case, especially for nested dependencies?

Closes #438

* fix(cli): typo

* chore(fmt): auto fixes from pre-commit.com hooks

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

* chore(tests): add tests

* fix(cli): use full path

* fix(tests): typo

* chore(ci): avoid specific kernel name

* fix ?

* chore(lib): simplify logic

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-01 10:00:48 +01:00
75af26e601 chore(docker): drop legacy syntax for env variables 2024-10-15 17:58:20 +02:00
a8903b809d chore(deps): bump version from 5.1.8 to 5.1.9 2024-10-15 17:24:48 +02:00
d813aaf313 chore(docs): add changelog entry 2024-10-15 17:24:37 +02:00
d5679924b9 fix(ci): docker image (#481)
* fix(ci): docker image

* fix(ci): test build docker

* fix(ci): typo :-(

* fix(ci): don't use `--system`
2024-10-15 17:22:03 +02:00
fb562d88ac chore(deps): bump version from 5.1.7 to 5.1.8 2024-10-02 19:56:23 +02:00
8a1fb4c259 chore(deps): pre-commit autoupdate (#479)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-30 23:03:39 +02:00
9db2358793 chore(deps): bump astral-sh/setup-uv from 2 to 3 (#477)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 2 to 3.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v2...v3)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  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-09-25 10:08:09 +02:00
de91ac7b7c chore(ci): from Rye to uv (#476)
* chore(ci): from Rye to uv

uv is just better for what I need to do, and Rye will eventually be replaced by uv anyway

* chore(ci): add tests' extra

* chore(ci): oops

* fix(ci): some tests

* chore(ci): remove -W error

Because it breaks on the CI, but not locally..
2024-09-24 17:15:18 +02:00
6ad89ecdf6 chore(docs): document new potential fix for PPTX (#475)
* chore(docs): document new potential fix for PPTX

Ref: https://github.com/jeertmans/manim-slides/issues/392#issuecomment-2368198106.

* chore(fmt): 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-09-24 16:14:34 +02:00
c98501b71c chore(deps): pre-commit autoupdate (#474)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.4...v0.6.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-24 15:22:32 +02:00
2d7d097276 fix(ci): wrong link 2024-09-23 10:22:24 +02:00
eeac42c638 chore(deps): pre-commit autoupdate (#471)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.3 → v0.6.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.3...v0.6.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-09-15 11:41:11 +02:00
ae42403962 chore(cli): switch option order 2024-09-11 15:57:21 +02:00
1539b2f7e4 feat(cli/convert): add HTML zip output (#472)
Closes #437
2024-09-11 15:30:38 +02:00
a39a9c2bb1 feat(lib): use and add more config option (#452)
* feat(lib): use and add more config option

Closes #441

* feat(lib): add support for more options and test them

* chore(docs): add attributes to documentation

* chore(docs): sort members by member type

* chore(docs): small improvements

* chore(docs): cleanup
2024-09-05 14:08:10 +02:00
87867762ea chore(deps): pre-commit autoupdate (#468)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-02 22:39:57 +02:00
2f4fe9bd06 fix(lib): deprecation warnings (#467)
* fix(lib): deprecation warnings

* fix(tests): collect tests in the correct order

* fix(tests): ignore pydub warning

* fix(tests): correctly ignore warnings

* fix(ci): do we need faulthandler?
2024-08-27 14:57:23 +02:00
924d8210d9 fix(cli): remove echo 2024-08-27 13:50:44 +02:00
59236b84db chore(deps): pre-commit autoupdate (#466)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2)
- [github.com/pre-commit/mirrors-mypy: v1.11.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.1...v1.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-08-27 12:55:59 +02:00
c047da67b1 feat(cli): manim-slides checkhealth (#458)
* feat(cli): `manim-slides checkhealth`

Closes #457

* chore(fmt): auto fixes from pre-commit.com hooks

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

* chore(tests): implement some basic tests

* chore(docs): document changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-27 12:23:24 +02:00
5b6f5eb1e4 fix(present): black video flashes and other bugs linked to Qt (#465)
* Relies on Pixel Format instead of Size

* chore(lib): remove deprecated warning

* chore(deps): update lockfiles

* chore(lib): cleanup code

* chore(ci): run fmt

* chore: update changelog

* chore: typo

* chore(docs): remove ref. to black screen

* fix(deps): issue on Windows

---------

Co-authored-by: PeculiarProgrammer <179261820+PeculiarProgrammer@users.noreply.github.com>
2024-08-26 17:29:39 +02:00
3b23c7739d chore(fmt): auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-08-23 21:17:15 +00:00
517fbc6240 Fix for #293 and similar errors
This commit fixes the screen going black after the video ends in versions of PySide6 v6.5.3 and newer. This allows for other errors (like #315) that require a newer version of PySide6 to also be fixed.
2024-08-23 17:05:27 -04:00
c3e1aa0276 chore(deps): pre-commit autoupdate (#463)
* chore(deps): pre-commit autoupdate

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

* fix(ci): ignore F403 and F405 in notebooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-08-20 11:31:13 +02:00
3e7174c331 chore(deps): pre-commit autoupdate (#459)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.13.0 → v2.14.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.13.0...v2.14.0)
- [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.6)
- [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.11.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-09 13:20:42 +02:00
6572dc3e21 chore(lib): enhance error message for --folder (#462)
Closes #461.
2024-08-07 12:05:53 +02:00
f707b6cd0c fix(docs): broken link 2024-07-23 14:38:54 +02:00
98f6dbd864 chore(docs): add citing 2024-07-23 14:35:23 +02:00
8c0dd6a605 chore(ci): update pre-commit ci config 2024-07-19 15:54:30 +02:00
ba3756b314 fix(ci): typo 2024-07-19 15:47:45 +02:00
1413a5eb9e fix(ci): move placeholder to correct input 2024-07-19 15:46:57 +02:00
467f744450 fix(ci): label name 2024-07-19 15:43:50 +02:00
4fe347e66a fix(ci): removing template and changing input types 2024-07-19 15:42:35 +02:00
9b62b62386 chore(ci): rework issue templates (#456)
* chore(ci): rework issue templates

Closes #406

* chore(ci): small fixes

* up

* add changelog entry
2024-07-19 15:32:26 +02:00
e2d8c5667f fix(lib): Manim fixes, bump to >= 0.18, and tests (#447)
* fix(lib): Manim fixes, bump to >= 0.18, and tests

* chore(ci): tests and happy mypy

* chore(deps): fix override

* fix(tests): correct skipping

* fix(ci): coverage

* fix(docs): dead links

* fix(tests): deps fixes

* fix(deps): add missing override

* fix(tests): correctly ignore

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

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

* chore(tests): no filterwarning

* chore(tests): add a check to see if we can install package

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

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

* chore(ci): typo

* fix(ci): typo

* oops

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

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

* fix(ci): double quote instead of single

* chore(ci): add OSes requirements

* chore(tests): removed `full-gl` extra

* chore(ci): automatically cancel jobs

* fix(docs): typo

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-01 18:19:24 +02:00
e80d1d08eb fix(ci): add missing 'v' in tag 2024-06-27 09:57:17 +02:00
5924c329a8 [pre-commit.ci] pre-commit autoupdate (#450)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-26 10:57:46 +02:00
20f91a4590 chore(deps): bump docker/build-push-action from 5 to 6 (#451)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-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-06-18 08:13:40 +02:00
964de66563 chore(deps): pin rtoml==0.9.0 on Windows (#432)
* chore(deps): pin `rtoml==0.9.0` on Windows

See #398 and https://github.com/samuelcolvin/rtoml/issues/74.

* chore(ci): trying to fix on Python 3.12
2024-06-12 10:32:50 +02:00
89377e3a55 [pre-commit.ci] pre-commit autoupdate (#444)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.8)
- [github.com/codespell-project/codespell: v2.2.6 → v2.3.0](https://github.com/codespell-project/codespell/compare/v2.2.6...v2.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-12 10:32:23 +02:00
0aa624c8ad chore(lib): Remove old validation check preventing loop=True with auto_next=True (#445)
* remove old validation check

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

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

* Update CHANGELOG.md

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-02 13:47:49 +02:00
9460f6b135 fix(convert): Whitespace issue in default RevealJS template (#442)
* Fix whitespace issue in RevealJS template

* Edit changelog
2024-05-27 20:49:14 +02:00
c0a240d758 fix(ci): update rye url
See https://github.com/astral-sh/rye/issues/1111
2024-05-24 21:45:45 +02:00
b08073983b fix(lib): prevent filename collision (#429)
* fix(lib): prevent filename collision

Apparently, ManimCE can produce two different animations with the same name (i.e., the same hash). As documented, ManimGL would any produce files with the same name so this fix was needed.

Closes #428

* chore(lib): update comment

chore(lib): update comment

* chore(tests): add test

* chore(tests): remove redundant underscore

* chore(docs): add changelog entry
2024-05-18 10:17:25 +02:00
993acf0e3f [pre-commit.ci] pre-commit autoupdate (#427)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-18 10:09:41 +02:00
962ff2bffe chore(deps): bump version from 5.1.6 to 5.1.7 2024-05-03 13:48:01 +02:00
9f2e4757ec chore(ci): fancy README and better bump config (#425)
* chore(ci): fancy README and better bump config

* fix(ci): CITATION.cff

* chore(docs): add changelog entry
2024-05-03 13:47:44 +02:00
6d85222fdc chore(ci): update changelog 2024-04-30 10:28:34 +02:00
19f60bf880 chore(version): bump 5.1.5 to 5.1.6 2024-04-30 10:26:26 +02:00
554f076a25 [pre-commit.ci] pre-commit autoupdate (#424)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.2)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)

* chore(lint): apply suggestions

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-04-30 10:22:49 +02:00
676acc6e3b fix(docs): typos in example 2024-04-30 10:13:17 +02:00
26c3fdca75 fix(ci): remove unused imports 2024-04-27 10:03:05 +02:00
f17e855323 feat(lib): add options for skipping Sphinx directive (#423) 2024-04-27 09:54:59 +02:00
73490298b3 chore(docs): add an Examples Gallery (#422)
* chore(docs): add an Examples Gallery

* chore(docs): add changelog entry
2024-04-27 09:51:55 +02:00
c9a06e45aa fix(docs): Update CHANGELOG.md 2024-04-23 08:42:04 +02:00
45269b9d1c [pre-commit.ci] pre-commit autoupdate (#421)
* [pre-commit.ci] pre-commit autoupdate

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

* [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-04-23 08:40:39 +02:00
63 changed files with 6835 additions and 1613 deletions

View File

@ -1,16 +0,0 @@
[bumpversion]
current_version = 5.1.5
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?
serialize =
{major}.{minor}.{patch}-rc{release}
{major}.{minor}.{patch}
commit = True
message = chore(version): bump {current_version} to {new_version}
[bumpversion:file:manim_slides/__version__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
[bumpversion:file:CITATION.cff]
search = version: v{current_version}
replace = version: v{new_version}

View File

@ -1,40 +1,177 @@
name: Bug
description: Report an issue to help improve the project.
labels: bug
title: '[BUG] <description>'
title: '[BUG] <short-description-here>'
labels: [bug]
body:
- type: markdown
id: preamble
attributes:
value: |
**Thank you for reporting a problem about Manim Slides!**
If you know how to solve your problem, feel free to submit a PR too!
> [!WARNING]
> Before reporting your bug, please make sure to:
>
> 1. create and activate virtual environment (venv);
> 2. install `manim-slides` and the necessary dependencies;
> 3. and reduce your Python to a minimal working example (MWE).
>
> You can skip the last step if your issue occurs during installation.
- type: checkboxes
id: terms
attributes:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- label: Checked the [frequently asked questions](https://manim-slides.eertmans.be/latest/faq.html);
required: true
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
required: true
- label: Created a virtual environment in which I can reproduce my bug;
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
label: Describe the issue
description: A description of the issue, also include what you tried and what didn't work.
validations:
required: true
- type: textarea
id: version
- type: input
id: command
attributes:
label: Version
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
label: Command
description: |
Enter the command that failed.
This will be automatically formatted into code, so no need for backticks.
placeholder: manim-slides render mwe.py MWE
validations:
required: true
- type: dropdown
id: issue-type
attributes:
label: Issue Type
description: >
Please select the option in the drop-down.
options:
- Installation issue
- Visual bug when presenting (`manim-slides present`)
- Bug when presenting with HTML/PowerPoint/... format (`manim-slides convert`)
- Other
validations:
required: true
- type: input
id: py-version
attributes:
label: Python version
description: |
Please copy and paste the output of `python --version`.
Make sure to activate your virtual environment first (if any).
placeholder: Python 3.11.8
validations:
required: true
- type: textarea
id: venv
attributes:
label: Python environment
description: |
Please copy and paste the output of `manim-slides checkhealth`.
Make sure to activate your virtual environment first (if any).
This will be automatically formatted into code, so no need for backticks.
If Manim Slides installation failed, enter 'N/A' instead.
render: shell
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: What is your platform. Linux, macOS, or Windows?
label: What is your platform?
multiple: true
options:
- Linux
- macOS
- Windows
- Other (please precise below)
validations:
required: true
- type: input
id: platform-other
attributes:
label: Other platform
description: Please answer if you have replied *Other* above.
validations:
required: false
- type: textarea
id: code
attributes:
label: Manim Slides Python code
description: |
Please copy and paste a minimal working example (MWE) of your Python code that can reproduce your bug.
This will be automatically formatted into code, so no need for backticks.
placeholder: |
from manim import *
from manim_slides import Slide
class MWE(Slide):
def construct(self):
circle = Circle(radius=2, color=RED)
dot = Dot()
self.play(GrowFromCenter(circle))
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=0.5)
self.next_slide()
self.play(dot.animate.move_to(ORIGIN))
render: python
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output.
This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
description: Please add screenshots if applicable.
validations:
required: false
- type: textarea
id: extrainfo
id: extra-info
attributes:
label: Additional information
description: Is there anything else we should know about this bug?
validations:
required: false
- type: textarea
id: suggested-fix
attributes:
label: Recommended fix or suggestions
description: A clear and concise description of how you want to update it.
validations:
required: false

View File

@ -1,57 +1,61 @@
name: Documentation
description: Ask / Report an issue related to the documentation.
title: 'DOC: <description>'
labels: [bug, docs]
title: '[DOC] <short-description-here>'
labels: [documentation]
body:
- type: markdown
id: preamble
attributes:
value: >
**Thank you for wanting to report a problem with manim-slides docs!**
value: |
**Thank you for reporting a problem about Manim Slides' documentation!**
If you know how to solve your problem, feel free to submit a PR too!
If the problem seems straightforward, feel free to submit a PR instead!
Verify first that your issue is not already reported on GitHub [Issues].
[Issues]:
https://github.com/jeertmans/manim-slides/issues
- type: checkboxes
id: terms
attributes:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- type: textarea
id: description
attributes:
label: Describe the Issue
label: Describe the issue
description: A clear and concise description of the issue you encountered.
validations:
required: true
- type: input
- type: textarea
id: pages
attributes:
label: Affected Page
description: Add a link to page with the problem.
label: Affected page(s)
description: Link to page(s) with the problem.
placeholder: |
+ https://manim-slides.eertmans.be/latest/installation.html
+ https://manim-slides.eertmans.be/latest/features_table.html
validations:
required: true
- type: dropdown
id: issue-type
attributes:
label: Issue Type
label: Issue type
description: >
Please select the option in the drop-down.
<details>
<summary>
<em>Issue?</em>
</summary>
</details>
options:
- Documentation Enhancement
- Documentation Report
- Typo, spelling mistake, broken link, etc.
- Something is missing
- Documentation enhancement
- Other
validations:
required: true
- type: textarea
id: suggested-fix
attributes:
label: Recommended fix or suggestions
description: A clear and concise description of how you want to update it.

View File

@ -1,8 +1,26 @@
name: Feature Request
name: Feature request
description: Have a new idea/feature? Please suggest!
labels: enhancement
title: '[FEATURE] <description>'
title: '[FEATURE] <short-description-here>'
labels: [enhancement]
body:
- type: markdown
id: preamble
attributes:
value: |
**Thank you for suggesting a new feature!**
If you know how to implement it, feel free to submit a PR too!
- type: checkboxes
id: terms
attributes:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Aenhancement+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- type: textarea
id: description
attributes:
@ -10,6 +28,7 @@ body:
description: A brief description of the enhancement you propose, also include what you tried and what worked.
validations:
required: true
- type: textarea
id: screenshots
attributes:
@ -17,6 +36,7 @@ body:
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:

View File

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

View File

@ -10,11 +10,13 @@
<!-- Describe all the proposed changes in your PR -->
## Check List (Check all the applicable boxes)
## Check List
- [ ] I understand that my contributions needs to pass the checks.
- [ ] If I created new functions / methods, I documented them and add type hints.
- [ ] If I modified already existing code, I updated the documentation accordingly.
Check all the applicable boxes:
- [ ] I understand that my contributions needs to pass the checks;
- [ ] If I created new functions / methods, I documented them and add type hints;
- [ ] If I modified already existing code, I updated the documentation accordingly;
- [ ] The title of my pull request is a short description of the requested changes.
## Screenshots

View File

@ -18,18 +18,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Rye
env:
RYE_INSTALL_OPTION: --yes
run: |
curl -sSf https://rye-up.com/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
- name: Configure Rye
run: rye config --set-bool behavior.use-uv=true
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Build package
run: rye build
run: uv build
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
@ -41,7 +36,6 @@ jobs:
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
@ -68,11 +62,11 @@ jobs:
print(f"tag_name={ref_tag}", file=f)
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
platforms: linux/arm64,linux/amd64
file: docker/Dockerfile
push: true
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
tags: |
ghcr.io/jeertmans/manim-slides:latest
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}

View File

@ -7,7 +7,54 @@ on:
name: Tests
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
pip-install:
strategy:
fail-fast: false
matrix:
os: [macos-13, ubuntu-latest, windows-latest]
pyversion: ['3.9', '3.10', '3.11', '3.12']
extras: [pyside6-full, manimgl]
exclude:
- pyversion: '3.12'
extras: manimgl
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.pyversion }}
cache: pip
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
run: brew install ffmpeg py3cairo pango pkg-config scipy
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get install build-essential python${{ matrix.pyversion }}-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
- name: Install package
shell: bash
env:
extras: ${{ matrix.extras }}
run: pip install ".[$extras]"
pytest:
strategy:
fail-fast: false
@ -18,47 +65,26 @@ jobs:
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: error
PYTHONFAULTHANDLER: 1
DISPLAY: :99
GITHUB_WORKFLOWS: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Rye
if: matrix.os != 'windows-latest'
env:
RYE_TOOLCHAIN_VERSION: ${{ matrix.pyversion}}
RYE_INSTALL_OPTION: --yes
run: |
curl -sSf https://rye-up.com/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
# Stolen from https://github.com/bluss/pyproject-local-kernel/blob/2b641290694adc998fb6bceea58d3737523a68b7/.github/workflows/ci.yaml
- name: Install Rye (Windows)
if: matrix.os == 'windows-latest'
shell: bash
run: |
C:/msys64/usr/bin/wget.exe -q 'https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe' -O rye-x86_64-windows.exe
./rye-x86_64-windows.exe self install --toolchain-version ${{ matrix.pyversion }} --modify-path -y
echo "$HOME\\.rye\\shims" >> $GITHUB_PATH
- name: Configure Rye
shell: bash
run: |
rye config --set-bool behavior.use-uv=true
rye pin ${{ matrix.pyversion }}
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
run: brew install ffmpeg py3cairo
run: brew install ffmpeg py3cairo pango pkg-config scipy
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
sudo apt-get install xvfb
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
@ -69,22 +95,11 @@ jobs:
if: matrix.os == 'windows-latest'
uses: ssciwr/setup-mesa-dist-win@v2
- name: Install Manim Slides
shell: bash
run: rye sync
- name: Run pytest
shell: bash
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
run: rye run pytest
- name: Run pytest and coverage
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
run: rye run pytest --cov-report xml --cov=manim_slides tests/
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
- name: Upload to codecov.io
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:

2
.gitignore vendored
View File

@ -4,7 +4,6 @@ __pycache__/
/build
/dist
*.egg-info/
.pdm-python
# Manim files
images/
@ -45,6 +44,7 @@ paper/paper.pdf
paper/media/
# Others
.coverage*
coverage.xml
rendering_times.csv

View File

@ -1,36 +1,38 @@
ci:
autofix_commit_msg: |
chore(fmt): auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.13.0
rev: v2.14.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
- id: pretty-format-toml
exclude: poetry.lock
args: [--autofix, --trailing-commas]
- repo: https://github.com/keewis/blackdoc
rev: v0.3.9
hooks:
- id: blackdoc
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
rev: v0.9.2
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
rev: v1.14.1
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies:

View File

@ -1 +1 @@
3.11.8
3.11

View File

@ -2,13 +2,10 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: '3.10'
python: '3.11'
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_install:
- ipython kernel install --name "manim-slides" --user
sphinx:
builder: html
configuration: docs/source/conf.py
@ -19,5 +16,3 @@ python:
path: .
extra_requirements:
- docs
- magic
- sphinx-directive

View File

@ -8,13 +8,220 @@ 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.5...HEAD)
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.4.1...HEAD)
(v5.4.1)=
## [v5.4.1](https://github.com/jeertmans/manim-slides/compare/v5.4.0...v5.4.1)
(v5.4.1-added)=
### Added
- Added `start_skip_animations` and `stop_skip_animations` methods.
[#523](https://github.com/jeertmans/manim-slides/pull/523)
(v5.4.0)=
## [v5.4.0](https://github.com/jeertmans/manim-slides/compare/v5.3.1...v5.4.0)
(v5.4.0-added)=
### Added
- Added `skip_animations` compatibility with ManimCE.
[@Rapsssito](https://github.com/Rapsssito) [#516](https://github.com/jeertmans/manim-slides/pull/516)
(v5.4.0-chore)=
### Chore
- Bumped Manim to `>=0.19`, as it fixed OpenGL renderer issue.
[#522](https://github.com/jeertmans/manim-slides/pull/522)
(v5.4.0-fixed)=
### Fixed
- Fixed OpenGL renderer having no partial movie files with Manim bindings.
[#522](https://github.com/jeertmans/manim-slides/pull/522)
- Fixed `ConvertExample` example as `manim>=0.19` changed the `Code` class.
[#522](https://github.com/jeertmans/manim-slides/pull/522)
(v5.3.1)=
## [v5.3.1](https://github.com/jeertmans/manim-slides/compare/v5.3.0...v5.3.1)
(v5.3.1-fixed)=
### Fixed
- Fixed HTML template to avoid missing slides when exporting with `--one-file`.
[@Rapsssito](https://github.com/Rapsssito) [#515](https://github.com/jeertmans/manim-slides/pull/515)
(v5.3.0)=
## [v5.3.0](https://github.com/jeertmans/manim-slides/compare/v5.2.0...v5.3.0)
(v5.3.0-added)=
### Added
- Added CSS and JS inline for `manim-slides convert` if `--offline`
and `--one-file` (`-cone_file`) are used for HTML output.
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
(v5.3.0-changed)=
### Changed
- Deprecate `-cdata_uri` in favor of `-cone_file` for `manim-slides convert`.
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
- Changed template to avoid micro-stuttering with `--one-file` in HTML presentation.
[@Rapsssito](https://github.com/Rapsssito) [#508](https://github.com/jeertmans/manim-slides/pull/508)
(v5.2.0)=
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
(v5.2.0-changed)=
### Changed
- The info window is now only shown in presentations when there
are multiple monitors. However, the `--show-info-window` option
was added to `manim-slides present` to force the info window.
When there are multiple monitors, the info window will no longer
be on the same monitor as the main window, unless overridden.
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
[#482](https://github.com/jeertmans/manim-slides/pull/482)
(v5.2.0-chore)=
### Chore
- Bumped ManimGL to `>=1.7.1`, to remove conflicting dependencies
with Manim's.
[#499](https://github.com/jeertmans/manim-slides/pull/499)
- Bumped ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies,
and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`,
as the latter includes many breaking changes.
[#506](https://github.com/jeertmans/manim-slides/pull/506)
(v5.1.10)=
## [v5.1.10](https://github.com/jeertmans/manim-slides/compare/v5.1.9...v5.1.10)
(v5.1.10-added)=
### Added
- Added `--offline` option to `manim-slides convert` for offline
HTML presentations.
[#440](https://github.com/jeertmans/manim-slides/pull/440)
- Added documentation to config option to `manim-slides convert`
when using `--show-config`.
[#485](https://github.com/jeertmans/manim-slides/pull/485)
(v5.1.10-changed)=
### Changed
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
[#488](https://github.com/jeertmans/manim-slides/pull/488)
(v5.1.10-fixed)=
### Fixed
- Fixed PyAV issue by pinning its version to `<14`.
A future release will contain a fix that supports both `av>=14`
and `av<14`, as their syntax differ, but the former doesn't
provide binary wheels for Python 3.9.
[#494](https://github.com/jeertmans/manim-slides/pull/494)
- Fixed blank web page when converting multiple slides into HTML.
[#497](https://github.com/jeertmans/manim-slides/pull/497)
(v5.1.9)=
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)
(v5.1.9-fixed)=
## Chore
- Fixed failing docker builds.
[#481](https://github.com/jeertmans/manim-slides/pull/481)
(v5.1.8)=
## [v5.1.8](https://github.com/jeertmans/manim-slides/compare/v5.1.7...v5.1.8)
(v5.1.8-added)=
### Added
- Added `manim-slides checkhealth` command to easily obtain important information
for debug purposes.
[#458](https://github.com/jeertmans/manim-slides/pull/458)
- Added support for `disable_caching` and `flush_cache` options from Manim, and
also the possibility to configure them through class options.
[#452](https://github.com/jeertmans/manim-slides/pull/452)
- Added `--to=zip` convert format to generate an archive with HTML output
and asset files.
[#470](https://github.com/jeertmans/manim-slides/pull/470)
(v5.1.8-chore)=
### Chore
- Pinned `rtoml==0.9.0` on Windows platforms,
see [#398](https://github.com/jeertmans/manim-slides/pull/398),
until
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
is solved.
[#432](https://github.com/jeertmans/manim-slides/pull/432)
- Removed an old validation check that prevented setting `loop=True` with
`auto_next=True` on `next_slide()`
[#445](https://github.com/jeertmans/manim-slides/pull/445)
- Improved (and fixed) tests for Manim(GL), bumped minimal ManimCE version,
improved coverage, and override dependency conflicts.
[#447](https://github.com/jeertmans/manim-slides/pull/447)
- Improved issue templates.
[#456](https://github.com/jeertmans/manim-slides/pull/456)
- Enhanced the error message when the slides folder does not exist.
[#462](https://github.com/jeertmans/manim-slides/pull/462)
- Fixed deprecation warnings.
[#467](https://github.com/jeertmans/manim-slides/pull/467)
- Documented potential fix for PPTX issue.
[#475](https://github.com/jeertmans/manim-slides/pull/475)
- Changed project manager from Rye to uv.
[#476](https://github.com/jeertmans/manim-slides/pull/476)
(v5.1.8-fixed)=
### Fixed
- Fix combining assets from multiple scenes to avoid filename collision.
[#429](https://github.com/jeertmans/manim-slides/pull/429)
- Fixed whitespace issue in default RevealJS template.
[#442](https://github.com/jeertmans/manim-slides/pull/442)
- Fixed black screen issue on recent Qt versions and device loss detected,
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
[#465](https://github.com/jeertmans/manim-slides/pull/465)
(v5.1.8-removed)=
### Removed
- Removed `full-gl` extra, because it does not make sense to ship both
`manimgl` and `manim` together.
[#447](https://github.com/jeertmans/manim-slides/pull/447)
(v5.1.7)=
## [v5.1.7](https://github.com/jeertmans/manim-slides/compare/v5.1.6...v5.1.7)
(v5.1.7-chore)=
### Chore
- Improved the CI for bumping the version and README rendering on PyPI.
[#425](https://github.com/jeertmans/manim-slides/pull/425)
(v5.1.6)=
## [v5.1.6](https://github.com/jeertmans/manim-slides/compare/v5.1.5...v5.1.6)
(v5.1.6-added)=
### Added
- Added options to skip the Manim Slides Sphinx directive.
[#423](https://github.com/jeertmans/manim-slides/pull/423)
(v5.1.6-chore)=
### Chore
- Added an examples gallery.
[#422](https://github.com/jeertmans/manim-slides/pull/422)
(v5.1.5)=
## [v5.1.5](https://github.com/jeertmans/manim-slides/compare/v5.1.4...v5.1.5)
(v5.1.5)=
(v5.1.5-chore)=
### Chore
- Added CI for broken HTML links and fixed, plus spell checking.
@ -24,13 +231,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Used Rye instead of PDM for faster development.
[#420](https://github.com/jeertmans/manim-slides/pull/420)
(v5.1.5)=
(v5.1.5-fixed)=
### Fixed
- Fixed broken `--show-config` command.
[#419](https://github.com/jeertmans/manim-slides/pull/419)
(v5.1.4)=
## [v5.1.4](https://github.com/jeertmans/manim-slides/compare/v5.1.3...v5.1.4)

View File

@ -26,7 +26,7 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.1.5
version: v5.4.1
preferred-citation:
publisher:
name: The Open Journal

View File

@ -1,9 +1,17 @@
> [!IMPORTANT]
> Take the [**Manim Slides Survey**](https://forms.gle/i4scrwPQghbTQwQs5)
> to help improve this tool! Thanks in advance to all the people taking the time
> to answer this short survey! The form is open until **January 31st 2025**,
> and results will be communicated in the GitHub discussions.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
</picture>
<!-- start pypi -->
[![Latest Release][pypi-version-badge]][pypi-version-url]
[![Python version][pypi-python-version-badge]][pypi-version-url]
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
@ -29,6 +37,7 @@ Manim Slides will *automatically* detect the one you are using!
- [Usage](#usage)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [F.A.Q](https://eertmans.be/manim-slides/latest/faq.html)
- [Citing](#citing)
- [Contributing](#contributing)
* [Reporting an Issue](#reporting-an-issue)
* [Seeking for Help](#seeking-for-help)
@ -149,6 +158,24 @@ Below is a comparison of the most used ones with Manim Slides:
| Web Browser presentations | Yes | No | Yes | No |
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
## Citing
If you use this project, please cite it using the following reference:
```bibtex
@article{Jerome_Eertmans_Manim_Slides_A_2023,
title = {{Manim Slides: A Python package for presenting Manim content anywhere}},
author = {{Jérome Eertmans}},
year = 2023,
month = aug,
journal = {Journal of Open Source Education},
volume = 6,
doi = {10.21105/jose.00206}
}
```
or by linking this GitHub repository at the end of the presentation.
## Contributing
Contributions are more than welcome! Please read through

View File

@ -1,2 +1,2 @@
style:
camera:
background_color: '#000000'

View File

@ -1,9 +1,8 @@
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
FROM python:3.11-slim
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y \
ffmpeg \
build-essential \
gcc \
cmake \
@ -24,21 +23,22 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/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 \
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
mathastext microtype ms physics preview ragged2e relsize rsfs \
mathastext microtype multitoc physics prelim2e 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]
RUN pip install --no-cache-dir 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
ENV USER=${NB_USER}
ENV NB_UID=${NB_UID}
ENV HOME=/manim-slides
RUN adduser --disabled-password \
--gecos "Default user" \

View File

@ -21,7 +21,7 @@
{%- for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{%- if one_file -%}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% set file = assets_dir / slide_config.file.name %}
@ -32,7 +32,7 @@
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif -%}
{%- endif %}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
@ -315,27 +315,48 @@
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 one_file %}
// Fix found by @t-fritsch and @Rapsssito on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
function setVideoBase64(video) {
const sources = video.querySelectorAll('source');
// Update the source of the video
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}`);
function fixBase64VideoBackground(event) {
// Analyze all slides backgrounds
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
// Get the slide video and its sources for each background
const video = slide.querySelector('video');
if (video) {
setVideoBase64(video);
} else {
// Listen to the creation of the video element
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
if (addedNode.tagName === 'VIDEO') {
setVideoBase64(addedNode);
observer.disconnect(); // Stop observing once the video is handled
}
}
}
}
});
observer.observe(slide, { childList: true, subtree: true });
}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
}
// Setup base64 videos
Reveal.on( 'ready', fixBase64VideoBackground );
{% endif %}
</script>

View File

@ -30,9 +30,11 @@ extensions = [
# Additional
"nbsphinx",
"myst_parser",
"sphinxcontrib.programoutput",
"sphinxext.opengraph",
"sphinx_click",
"sphinx_copybutton",
"sphinx_design",
# Custom
"manim_slides.docs.manim_slides_directive",
]

View File

@ -24,25 +24,30 @@ the repository, and clone it locally.
As for every Python project, using virtual environment is recommended to avoid
conflicts between modules.
For this project, we use [Rye](https://rye-up.com/) to easily manage project
For this project, we use [uv](https://github.com/astral-sh/uv) to easily manage project
and development dependencies. If not already, please install this tool.
## Installing Python modules
With Rye, installation becomes straightforward:
With uv, installation becomes straightforward:
```bash
rye sync --all-features
uv sync --all-extras
```
:::{note}
You still need the same dependencies as to install Manim and ManimGL,
so please check their respective installation guides.
:::
## Running commands
Because modules are installed in a new Python environment,
you cannot use them directly in the shell.
Instead, you either need to prepend `rye run` to any command, e.g.:
Instead, you either need to prepend `uv run` to any command, e.g.:
```bash
rye run manim-slides wizard
uv run manim-slides wizard
```
## Testing your code
@ -51,7 +56,7 @@ Most of the tests are done with GitHub actions, thus not on your computer.
The only command you should run locally is:
```bash
rye run pre-commit run --all-files
uv run pre-commit run --all-files
```
This runs a few linter and formatter to make sure the code quality and style stay
@ -61,7 +66,7 @@ If a warning or an error is displayed, please fix it before going to next step.
For testing your code, simply run:
```bash
rye run pytest
uv run pytest
```
## Building the documentation
@ -73,7 +78,7 @@ To generate the documentation, run the following:
```bash
cd docs
rye run make html
uv run make html
```
Then, the output index file is located at `docs/build/html/index.html` and

View File

@ -29,13 +29,8 @@ ManimGL support is only guaranteed to work
on a very minimal set of versions, because it differs quite a lot from ManimCE,
and its development is not very active.
The typical issues are that (1) ManimGL needs an outdated NumPy version
and (2) ManimGL **should not** be installed from the GitHub repository,
at least not from the `main` branch, but from a version released to PyPI.
To solve the NumPy issue, you can safely downgrade NumPy to a version supported
by ManimGL,
while ignoring the possible *conflicting dependencies* messages from `pip` (or else).
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
### Presenting
@ -54,9 +49,12 @@ with ManimCE or ManimGL.
### Slides go black when video finishes
This is an issue with Qt,
which cannot be solve on all platforms and Python versions,
which cannot be solved on all platforms and Python versions,
see [#293](https://github.com/jeertmans/manim-slides/issues/293).
Recent version of Manim Slides, i.e., `manim-slides>5.1.7`, come
with a fix that should work fine.
### How to increase quality on Windows
On Windows platform, one may encounter a lower image resolution than expected.
@ -104,7 +102,7 @@ Questions related to `manim-slides convert [SCENES]... output.html`.
### I moved my `.html` file and it stopped working
If you did not specify `-cdata_uri=true` when converting,
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
then Manim Slides generated a folder containing all
the video files, in the same folder as the HTML
output. As the path to video files is a relative path,
@ -120,3 +118,7 @@ This issue is (probably) caused by PowerPoint never freeing
memory, causing memory allocation errors, and can be partially
solved by reducing the video quality or the number of slides,
see [#392](https://github.com/jeertmans/manim-slides/issues/392).
Another solution, suggested by [@Azercoco](https://github.com/Azercoco) in
[#392 (comment)](https://github.com/jeertmans/manim-slides/issues/392#issuecomment-2368198106),
is to disable hardware/GPU acceleration.

75
docs/source/gallery.md Normal file
View File

@ -0,0 +1,75 @@
# Examples Gallery
With Manim, the only limit to what you can create is your imagination!
*This also applies to Manim Slides.*
As the field of possibilities is infinitely vast,
it's often useful to **learn** how to use Manim Slides **based on examples**.
The aim of this page is to share with you the creations of some
Manim Slides users, to hopefully inspire you!
Most of them use HTML conversion to make them accessible via a website.
If you too have created content with Manim Slides that is available online
(e.g., a YouTube video or website),
don't hesitate to contact us so that we can share your content on this page!
## Scientif Research
Below are people that dissimenate their research results
using Manim Slides presentations.
### Daniel Panizo Pérez
Daniel publishes his presentations on *Cosmology, String Theory and related*
topics on his
[personal website](https://panopepino.github.io/web_page/main_page/slides.html). https://panopepino.github.io/web_page/main_page/slides.html
For example, below are the slides of a seminar he gave titled
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
<div style="position:relative;padding-bottom:56.25%;">
<iframe
loading="lazy"
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
frameborder="1"
width="100%"
height="100%"
allowfullscreen
allow="autoplay"
src="https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html">
</iframe>
</div>
He also shares his code on a public
[GitHub repository](https://github.com/PanoPepino/mtheoretical).
### Jérome Eertmans
Jérome, the author of Manim Slides, publishes his presentations
on the topic of *Ray Tracing applied to Radio Propagations* on his
[personal website](https://eertmans.be). He also uses Manim Slides
for presenting at conferences using the *PowerPoint* or HTML conversion.
For example, below are the slides of his
[PhD confirmation](https://eertmans.be/posts/confirmation2023-presentation/).
<div style="position:relative;padding-bottom:56.25%;">
<iframe
loading="lazy"
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
frameborder="1"
width="100%"
height="100%"
allowfullscreen
allow="autoplay"
src="https://eertmans.be/assets/slides/2023-12-07-confirmation.html">
</iframe>
</div>
## School Work
Below are people that used Manim Slides for school presentations.
*This list is currently empty. Please reach out to us if you have examples
to share!*

View File

@ -43,6 +43,7 @@ installation
reference/index
features_table
manim_or_manimgl
gallery
faq
```

View File

@ -29,19 +29,6 @@ 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
@ -108,17 +95,12 @@ using optional dependencies:
- `full`, to include `magic`, `manim`, and
`sphinx-directive` extras (see below);
- `full-gl`, to include `magic`, `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` to include PyQt6 Qt bindings;
- `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];
@ -142,12 +124,12 @@ Manim Slides is distributed under Nixpkgs >=24.05.
If you are using Nix or NixOS, you can find Manim Slides under:
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
includes pyqt6 (see above);
includes PyQt6 (see above);
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
module (for notebook magics), and includes IPython but not does not include
any Qt bindings.
module (for notebook magics), and includes IPython but does not include
any Qt binding.
You can try out the Manim Slides package with
You can try out the Manim Slides package with:
```sh
nix-shell -p manim ffmpeg manim-slides
@ -165,7 +147,7 @@ nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
:::{note}
Nix current does not support `manimgl`.
Nix does not currently support `manimgl`.
:::
## When you need a Qt backend

View File

@ -18,6 +18,8 @@ use, not the methods used internally when rendering.
next_section,
next_slide,
remove_from_canvas,
start_skip_animations,
stop_skip_animations,
wait_time_between_slides,
wipe,
zoom,

View File

@ -8,3 +8,30 @@ This page contains an exhaustive list of all the commands available with `manim-
:prog: manim-slides
:nested: full
```
## All config options
Each converter has its own configuration options, which are listed below.
::::{dropdown} HTML
```{program-output} manim-slides convert --to=html --show-config
```
::::
::::{dropdown} Zip
:::{note}
The Zip converter inherits from the HTML converter.
:::
```{program-output} manim-slides convert --to=zip --show-config
```
::::
::::{dropdown} PDF
```{program-output} manim-slides convert --to=pdf --show-config
```
::::
::::{dropdown} HTML
```{program-output} manim-slides convert --to=pdf --show-config
```
::::

View File

@ -78,9 +78,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"display_name": ".venv",
"language": "python",
"name": "manim-slides"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
@ -92,7 +92,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.11.8"
}
},
"nbformat": 4,

View File

@ -137,9 +137,10 @@ 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`),
all animations will be data URI encoded, making the HTML a self-contained
presentation file that can be shared on its own.
If you set the `--one-file` flag, all animations will be data URI encoded,
making the HTML a self-contained presentation file that can be shared
on its own. If you also set the `--offline` flag, the JS and CSS files will
be included in the HTML file as well.
### Over the internet
@ -166,7 +167,7 @@ Pages. Please refer to the template page for usage instructions.
### With PowerPoint (*EXPERIMENTAL*)
A recent conversion feature is to the PowerPoint format, thanks to the
A convenient conversion feature is to the PowerPoint format, thanks to the
`python-pptx` package. Even though it is fully working,
it is still considered in an *EXPERIMENTAL* status because we do not
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.

View File

@ -40,7 +40,7 @@ class ConvertExample(Slide):
step_2 = Text("2. Replace Scene with Slide")
step_3 = Text("3. In construct, add pauses where you need")
step_4 = Text("4. You can also create loops")
step_5 = Text("5. Render you scene with Manim")
step_5 = Text("5. Render your scene with Manim")
step_6 = Text("6. Open your presentation with Manim Slides")
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
@ -53,7 +53,7 @@ class ConvertExample(Slide):
self.next_slide()
code = Code(
code="""from manim import *
code_string="""from manim import *
class Example(Scene):
@ -72,7 +72,7 @@ class Example(Scene):
)
code_step_1 = Code(
code="""from manim import *
code_string="""from manim import *
from manim_slides import Slide
class Example(Scene):
@ -91,7 +91,7 @@ class Example(Scene):
)
code_step_2 = Code(
code="""from manim import *
code_string="""from manim import *
from manim_slides import Slide
class Example(Slide):
@ -110,7 +110,7 @@ class Example(Slide):
)
code_step_3 = Code(
code="""from manim import *
code_string="""from manim import *
from manim_slides import Slide
class Example(Slide):
@ -129,7 +129,7 @@ class Example(Slide):
)
code_step_4 = Code(
code="""from manim import *
code_string="""from manim import *
from manim_slides import Slide
class Example(Slide):
@ -148,19 +148,19 @@ class Example(Slide):
)
code_step_5 = Code(
code="manim example.py Example",
code_string="manim-slide render example.py Example",
language="console",
)
code_step_6 = Code(
code="manim-slides Example",
code_string="manim-slides Example",
language="console",
)
or_text = Text("or generate HTML presentation").scale(0.5)
code_step_7 = Code(
code="manim-slides convert Example slides.html --open",
code_string="manim-slides convert Example slides.html --open",
language="console",
).shift(DOWN)
@ -191,7 +191,7 @@ class Example(Slide):
self.play(Transform(code, code_step_6))
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
watch_text = Text("Watch results on next slides!").shift(2 * DOWN).scale(0.5)
self.next_slide(loop=True)
self.play(FadeIn(watch_text))

View File

@ -1,6 +1,6 @@
import sys
from types import ModuleType
from typing import Any, List
from typing import Any
from .__version__ import __version__

View File

@ -5,6 +5,7 @@ import requests
from click_default_group import DefaultGroup
from .__version__ import __version__
from .checkhealth import checkhealth
from .convert import convert
from .logger import logger
from .present import list_scenes, present
@ -63,6 +64,7 @@ def cli(notify_outdated_version: bool) -> None:
cli.add_command(convert)
cli.add_command(checkhealth)
cli.add_command(init)
cli.add_command(list_scenes)
cli.add_command(present)

View File

@ -1 +1 @@
__version__ = "5.1.5"
__version__ = "5.4.1"

View File

@ -0,0 +1,37 @@
import sys
import click
from .__version__ import __version__
@click.command()
def checkhealth() -> None:
"""Check Manim Slides' installation."""
click.echo(f"Manim Slides version: {__version__}")
click.echo(f"Python executable: {sys.executable}")
click.echo("Manim bindings:")
try:
from manim import __version__ as manimce_version
click.echo(f"\tmanim (version: {manimce_version})")
except ImportError:
click.secho("\tmanim not found", bold=True)
try:
from manimlib import __version__ as manimlib_version
click.echo(f"\tmanimgl (version: {manimlib_version})")
except ImportError:
click.secho("\tmanimgl not found", bold=True)
try:
from qtpy import API, QT_VERSION
click.echo(f"Qt API: {API} (version: {QT_VERSION})")
except ImportError:
click.secho(
"No Qt API found, some Manim Slides commands will not be available",
bold=True,
)

View File

@ -70,11 +70,22 @@ def verbosity_option(function: F) -> F:
def folder_path_option(function: F) -> F:
"""Wrap a function to add folder path option."""
def callback(ctx: Context, param: Parameter, value: Path) -> Path:
if not value.exists():
raise click.UsageError(
f"Invalid value for '--folder': Directory '{value}' does not exist. "
"Did you render the animations first?",
ctx=ctx,
)
return value
wrapper: Wrapper = click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False, path_type=Path),
type=click.Path(file_okay=False, path_type=Path),
callback=callback,
help="Set slides folder.",
show_default=True,
)

View File

@ -13,6 +13,8 @@ from pydantic import (
FilePath,
PositiveInt,
PrivateAttr,
conset,
field_serializer,
field_validator,
model_validator,
)
@ -47,20 +49,13 @@ def key_id(name: str) -> PositiveInt:
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
ids: list[PositiveInt] = Field(unique=True)
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
name: Optional[str] = None
__signal: Signal = PrivateAttr(default_factory=Signal)
@field_validator("ids")
@classmethod
def ids_is_non_empty_set(cls, ids: set[Any]) -> set[Any]:
if len(ids) <= 0:
raise ValueError("Key's ids must be a non-empty set")
return ids
def set_ids(self, *ids: int) -> None:
self.ids = list(set(ids))
self.ids = set(ids)
def match(self, key_id: int) -> bool:
m = key_id in self.ids
@ -77,6 +72,10 @@ class Key(BaseModel): # type: ignore[misc]
def connect(self, function: Receiver) -> None:
self.__signal.connect(function)
@field_serializer("ids")
def serialize_dt(self, ids: set[int]) -> list[int]:
return list(self.ids)
class Keys(BaseModel): # type: ignore[misc]
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
@ -161,6 +160,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
reversed_playback_rate: float = 1.0
notes: str = ""
dedent_notes: bool = True
skip_animations: bool = False
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
@ -180,7 +180,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
fun_kwargs = {
key: value
for key, value in kwargs.items()
if key not in cls.__fields__
if key not in cls.model_fields
}
fun_kwargs[arg_name] = cls(**kwargs)
return fun(*args, **fun_kwargs)
@ -194,7 +194,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
default=field_info.default,
annotation=field_info.annotation,
)
for field_name, field_info in cls.__fields__.items()
for field_name, field_info in cls.model_fields.items()
]
sig = sig.replace(parameters=parameters)
@ -231,7 +231,7 @@ class PreSlideConfig(BaseSlideConfig):
return cls(
start_animation=start_animation,
end_animation=end_animation,
**base_slide_config.dict(),
**base_slide_config.model_dump(),
)
@field_validator("start_animation", "end_animation")
@ -262,21 +262,6 @@ class PreSlideConfig(BaseSlideConfig):
return pre_slide_config
@model_validator(mode="after")
@classmethod
def loop_and_auto_next_disallowed(
cls, pre_slide_config: "PreSlideConfig"
) -> "PreSlideConfig":
if pre_slide_config.loop and pre_slide_config.auto_next:
raise ValueError(
"You cannot have both `loop=True` and `auto_next=True`, "
"because a looping slide has no ending. "
"This may be supported in the future if "
"https://github.com/jeertmans/manim-slides/pull/299 gets merged."
)
return pre_slide_config
@property
def slides_slice(self) -> slice:
return slice(self.start_animation, self.end_animation)
@ -292,7 +277,7 @@ class SlideConfig(BaseSlideConfig):
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, **pre_slide_config.dict())
return cls(file=file, rev_file=rev_file, **pre_slide_config.model_dump())
class PresentationConfig(BaseModel): # type: ignore[misc]
@ -324,23 +309,22 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
f.write(self.model_dump_json(indent=2))
def copy_to(
self, folder: Path, use_cached: bool = True, include_reversed: bool = True
) -> "PresentationConfig":
self,
folder: Path,
use_cached: bool = True,
include_reversed: bool = True,
prefix: str = "",
) -> None:
"""Copy the files to a given directory."""
for slide_config in self.slides:
file = slide_config.file
rev_file = slide_config.rev_file
dest = folder / file.name
rev_dest = folder / rev_file.name
slide_config.file = dest
slide_config.rev_file = rev_dest
dest = folder / f"{prefix}{file.name}"
rev_dest = folder / f"{prefix}{rev_file.name}"
if not use_cached or not dest.exists():
shutil.copy(file, dest)
if include_reversed and (not use_cached or not rev_dest.exists()):
shutil.copy(rev_file, rev_dest)
return self

View File

@ -1,8 +1,11 @@
import mimetypes
import os
import platform
import shutil
import subprocess
import tempfile
import textwrap
import warnings
import webbrowser
from base64 import b64encode
from collections import deque
@ -14,6 +17,8 @@ from typing import Any, Callable, Optional, Union
import av
import click
import pptx
import requests
from bs4 import BeautifulSoup
from click import Context, Parameter
from jinja2 import Template
from lxml import etree
@ -27,7 +32,6 @@ from pydantic import (
PositiveFloat,
PositiveInt,
ValidationError,
conlist,
)
from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.color import Color
@ -99,9 +103,12 @@ def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
class Converter(BaseModel): # type: ignore
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
assets_dir: str = "{basename}_assets"
template: Optional[Path] = None
presentation_configs: list[PresentationConfig]
assets_dir: str = Field(
"{basename}_assets",
description="Assets folder.\nThis is a template string that accepts 'dirname', 'basename', and 'ext' as variables.\nThose variables are obtained from the output filename.",
)
template: Optional[Path] = Field(None, description="Custom template file to use.")
def convert_to(self, dest: Path) -> None:
"""Convert self, i.e., a list of presentations, into a given format."""
@ -115,9 +122,9 @@ class Converter(BaseModel): # type: ignore
"""
return ""
def open(self, file: Path) -> Any:
def open(self, file: Path) -> None:
"""Open a file, generated with converter, using appropriate application."""
raise NotImplementedError
open_with_default(file)
@classmethod
def from_string(cls, s: str) -> type["Converter"]:
@ -126,6 +133,7 @@ class Converter(BaseModel): # type: ignore
"html": RevealJS,
"pdf": PDF,
"pptx": PowerPoint,
"zip": HtmlZip,
}[s]
@ -285,49 +293,153 @@ class RevealTheme(str, StrEnum):
class RevealJS(Converter):
# Export option: use data-uri
data_uri: bool = False
"""
RevealJS options.
Please check out https://revealjs.com/config/ for more details.
"""
# Export option:
one_file: bool = Field(
False, description="Embed all assets (e.g., animations) inside the HTML."
)
offline: bool = Field(
False, description="Download remote assets for offline presentation."
)
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
margin: float = 0.04
min_scale: float = 0.2
max_scale: float = 2.0
width: Union[Str, int] = Field(
Str("100%"), description="Width of the presentation."
)
height: Union[Str, int] = Field(
Str("100%"), description="Height of the presentation."
)
margin: float = Field(0.04, description="Margin to use around the content.")
min_scale: float = Field(
0.2, description="Bound for smallest possible scale to apply to content."
)
max_scale: float = Field(
2.0, description="Bound for large possible scale to apply to content."
)
# Configuration options from RevealJS
controls: JsBool = JsBool.false
controls_tutorial: JsBool = JsBool.true
controls_layout: ControlsLayout = ControlsLayout.bottom_right
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
progress: JsBool = JsBool.false
slide_number: SlideNumber = SlideNumber.false
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
hash_one_based_index: JsBool = JsBool.false
hash: JsBool = JsBool.false
respond_to_hash_changes: JsBool = JsBool.false
history: JsBool = JsBool.false
keyboard: JsBool = JsBool.true
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
disable_layout: JsBool = JsBool.false
overview: JsBool = JsBool.true
center: JsBool = JsBool.true
touch: JsBool = JsBool.true
loop: JsBool = JsBool.false
rtl: JsBool = JsBool.false
navigation_mode: NavigationMode = NavigationMode.default
shuffle: JsBool = JsBool.false
fragments: JsBool = JsBool.true
fragment_in_url: JsBool = JsBool.true
embedded: JsBool = JsBool.false
help: JsBool = JsBool.true
pause: JsBool = JsBool.true
show_notes: JsBool = JsBool.false
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
preload_iframes: PreloadIframes = PreloadIframes.null
auto_animate: JsBool = JsBool.true
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
auto_animate_duration: float = 1.0
auto_animate_unmatched: JsBool = JsBool.true
controls: JsBool = Field(
JsBool.false, description="Display presentation control arrows."
)
controls_tutorial: JsBool = Field(
JsBool.true, description="Help the user learn the controls by providing hints."
)
controls_layout: ControlsLayout = Field(
ControlsLayout.bottom_right, description="Determine where controls appear."
)
controls_back_arrows: ControlsBackArrows = Field(
ControlsBackArrows.faded,
description="Visibility rule for backwards navigation arrows.",
)
progress: JsBool = Field(
JsBool.false, description="Display a presentation progress bar."
)
slide_number: SlideNumber = Field(
SlideNumber.false, description="Display the page number of the current slide."
)
show_slide_number: Union[ShowSlideNumber, Function] = Field(
ShowSlideNumber.all,
description="Can be used to limit the contexts in which the slide number appears.",
)
hash_one_based_index: JsBool = Field(
JsBool.false,
description="Use 1 based indexing for # links to match slide number (default is zero based).",
)
hash: JsBool = Field(
JsBool.false,
description="Add the current slide number to the URL hash so that reloading the page/copying the URL will return you to the same slide.",
)
respond_to_hash_changes: JsBool = Field(
JsBool.false,
description="Flags if we should monitor the hash and change slides accordingly.",
)
jump_to_slide: JsBool = Field(
JsBool.true,
description="Enable support for jump-to-slide navigation shortcuts.",
)
history: JsBool = Field(
JsBool.false,
description="Push each slide change to the browser history. Implies `hash: true`.",
)
keyboard: JsBool = Field(
JsBool.true, description="Enable keyboard shortcuts for navigation."
)
keyboard_condition: Union[KeyboardCondition, Function] = Field(
KeyboardCondition.null,
description="Optional function that blocks keyboard events when retuning false.",
)
disable_layout: JsBool = Field(
JsBool.false,
description="Disable the default reveal.js slide layout (scaling and centering) so that you can use custom CSS layout.",
)
overview: JsBool = Field(JsBool.true, description="Enable the slide overview mode.")
center: JsBool = Field(JsBool.true, description="Vertical centering of slides.")
touch: JsBool = Field(
JsBool.true, description="Enable touch navigation on devices with touch input."
)
loop: JsBool = Field(JsBool.false, description="Loop the presentation.")
rtl: JsBool = Field(
JsBool.false, description="Change the presentation direction to be RTL."
)
navigation_mode: NavigationMode = Field(
NavigationMode.default,
description="Change the behavior of our navigation directions.",
)
shuffle: JsBool = Field(
JsBool.false,
description="Randomize the order of slides each time the presentation loads.",
)
fragments: JsBool = Field(
JsBool.true, description="Turns fragment on and off globally."
)
fragment_in_url: JsBool = Field(
JsBool.true,
description="Flag whether to include the current fragment in the URL, so that reloading brings you to the same fragment position.",
)
embedded: JsBool = Field(
JsBool.false,
description="Flag if the presentation is running in an embedded mode, i.e. contained within a limited portion of the screen.",
)
help: JsBool = Field(
JsBool.true,
description="Flag if we should show a help overlay when the question-mark key is pressed.",
)
pause: JsBool = Field(
JsBool.true,
description="Flag if it should be possible to pause the presentation (blackout).",
)
show_notes: JsBool = Field(
JsBool.false,
description="Flag if speaker notes should be visible to all viewers.",
)
auto_play_media: AutoPlayMedia = Field(
AutoPlayMedia.null,
description="Global override for autolaying embedded media (video/audio/iframe).",
)
preload_iframes: PreloadIframes = Field(
PreloadIframes.null,
description="Global override for preloading lazy-loaded iframes.",
)
auto_animate: JsBool = Field(
JsBool.true, description="Can be used to globally disable auto-animation."
)
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = Field(
AutoAnimateMatcher.null,
description="Optionally provide a custom element matcher that will be used to dictate which elements we can animate between.",
)
auto_animate_easing: AutoAnimateEasing = Field(
AutoAnimateEasing.ease,
description="Default settings for our auto-animate transitions, can be overridden per-slide or per-element via data arguments.",
)
auto_animate_duration: float = Field(
1.0, description="See 'auto_animate_easing' documentation."
)
auto_animate_unmatched: JsBool = Field(
JsBool.true, description="See 'auto_animate_easing' documentation."
)
auto_animate_styles: list[str] = Field(
default_factory=lambda: [
"opacity",
@ -342,34 +454,88 @@ class RevealJS(Converter):
"border-radius",
"outline",
"outline-offset",
]
],
description="CSS properties that can be auto-animated.",
)
auto_slide: AutoSlide = Field(
0, description="Control automatic progression to the next slide."
)
auto_slide_stoppable: JsBool = Field(
JsBool.true, description="Stop auto-sliding after user input."
)
auto_slide_method: Union[AutoSlideMethod, Function] = Field(
AutoSlideMethod.null,
description="Use this method for navigation when auto-sliding (defaults to navigateNext).",
)
default_timing: Union[JsNull, int] = Field(
JsNull.null,
description="Specify the average time in seconds that you think you will spend presenting each slide.",
)
mouse_wheel: JsBool = Field(
JsBool.false, description="Enable slide navigation via mouse wheel."
)
preview_links: JsBool = Field(
JsBool.false, description="Open links in an iframe preview overlay."
)
post_message: JsBool = Field(
JsBool.true, description="Expose the reveal.js API through window.postMessage."
)
post_message_events: JsBool = Field(
JsBool.false,
description="Dispatch all reveal.js events to the parent window through postMessage.",
)
focus_body_on_page_visibility_change: JsBool = Field(
JsBool.true,
description="Focus body when page changes visibility to ensure keyboard shortcuts work.",
)
transition: Transition = Field(Transition.none, description="Transition style.")
transition_speed: TransitionSpeed = Field(
TransitionSpeed.default, description="Transition speed."
)
background_size: BackgroundSize = Field(
BackgroundSize.contain, description="Background size attribute for each video."
) # Not in RevealJS
background_transition: BackgroundTransition = Field(
BackgroundTransition.none,
description="Transition style for full page slide backgrounds.",
)
pdf_max_pages_per_slide: Union[int, str] = Field(
"Number.POSITIVE_INFINITY",
description="The maximum number of pages a single slide can expand onto when printing to PDF, unlimited by default.",
)
pdf_separate_fragments: JsBool = Field(
JsBool.true, description="Print each fragment on a separate slide."
)
pdf_page_height_offset: int = Field(
-1,
description="Offset used to reduce the height of content within exported PDF pages.",
)
view_distance: int = Field(
3, description="Number of slides away from the current that are visible."
)
mobile_view_distance: int = Field(
2,
description="Number of slides away from the current that are visible on mobile devices.",
)
display: Display = Field(
Display.block, description="The display mode that will be used to show slides."
)
hide_inactive_cursor: JsBool = Field(
JsBool.true, description="Hide cursor if inactive."
)
hide_cursor_time: int = Field(
5000, description="Time before the cursor is hidden (in ms)."
)
auto_slide: AutoSlide = 0
auto_slide_stoppable: JsBool = JsBool.true
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
default_timing: Union[JsNull, int] = JsNull.null
mouse_wheel: JsBool = JsBool.false
preview_links: JsBool = JsBool.false
post_message: JsBool = JsBool.true
post_message_events: JsBool = JsBool.false
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
background_transition: BackgroundTransition = BackgroundTransition.none
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
pdf_separate_fragments: JsBool = JsBool.true
pdf_page_height_offset: int = -1
view_distance: int = 3
mobile_view_distance: int = 2
display: Display = Display.block
hide_inactive_cursor: JsBool = JsBool.true
hide_cursor_time: int = 5000
# Appearance options from RevealJS
background_color: Color = "black"
reveal_version: str = "5.1.0"
reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides"
background_color: Color = Field(
"black",
description="Background color used in slides, not relevant if videos fill the whole area.",
)
reveal_version: str = Field("5.1.0", description="RevealJS version.")
reveal_theme: RevealTheme = Field(
RevealTheme.black, description="RevealJS version."
)
title: str = Field("Manim Slides", description="Presentation title.")
# Pydantic options
model_config = ConfigDict(use_enum_values=True, extra="forbid")
@ -380,40 +546,60 @@ class RevealJS(Converter):
return resources.files(templates).joinpath("revealjs.html").read_text()
def open(self, file: Path) -> bool:
return webbrowser.open(file.absolute().as_uri())
def open(self, file: Path) -> None:
webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> None:
def convert_to(self, dest: Path) -> None: # noqa: C901
"""
Convert this configuration into a RevealJS HTML presentation, saved to
DEST.
"""
if self.data_uri:
assets_dir = Path("") # Actually we won't care.
else:
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
if not self.one_file or self.offline:
logger.debug(f"Assets will be saved to: {full_assets_dir}")
full_assets_dir.mkdir(parents=True, exist_ok=True)
if not self.one_file:
num_presentation_configs = len(self.presentation_configs)
for presentation_config in self.presentation_configs:
presentation_config.copy_to(full_assets_dir, include_reversed=False)
if num_presentation_configs > 1:
# Prevent possible name collision, see:
# https://github.com/jeertmans/manim-slides/issues/428
# With ManimCE, this can happen when caching is disabled as filenames are
# 'uncached_000x.mp4'
# With ManimGL, this can easily occur since filenames are just basic integers...
num_digits = len(str(num_presentation_configs - 1))
def prefix(i: int) -> str:
return f"s{i:0{num_digits}d}_"
else:
def prefix(i: int) -> str:
return ""
full_assets_dir.mkdir(parents=True, exist_ok=True)
for i, presentation_config in enumerate(self.presentation_configs):
presentation_config.copy_to(
full_assets_dir, include_reversed=False, prefix=prefix(i)
)
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "w") as f:
revealjs_template = Template(self.load_template())
options = self.dict()
options["assets_dir"] = assets_dir
options = self.model_dump()
if assets_dir is not None:
options["assets_dir"] = assets_dir
has_notes = any(
slide_config.notes != ""
@ -426,25 +612,89 @@ class RevealJS(Converter):
get_duration_ms=get_duration_ms,
has_notes=has_notes,
env=os.environ,
prefix=prefix if not self.one_file else None,
**options,
)
# If not offline, write the content to the file
if not self.offline:
f.write(content)
return
# If offline, download remote assets and store them in the assets folder
soup = BeautifulSoup(content, "html.parser")
session = requests.Session()
for tag, inner in [("link", "href"), ("script", "src")]:
for item in soup.find_all(tag):
if item.has_attr(inner) and (link := item[inner]).startswith(
"http"
):
asset_name = link.rsplit("/", 1)[1]
asset = session.get(link)
if self.one_file:
# If it is a CSS file, inline it
if tag == "link" and "stylesheet" in item["rel"]:
item.decompose()
style = soup.new_tag("style")
style.string = asset.text
soup.head.append(style)
# If it is a JS file, inline it
elif tag == "script":
item.decompose()
script = soup.new_tag("script")
script.string = asset.text
soup.head.append(script)
else:
raise ValueError(
f"Unable to inline {tag} asset: {link}"
)
else:
full_assets_dir.mkdir(parents=True, exist_ok=True)
with open(full_assets_dir / asset_name, "wb") as asset_file:
asset_file.write(asset.content)
item[inner] = str(assets_dir / asset_name)
content = str(soup)
f.write(content)
class HtmlZip(RevealJS):
def open(self, file: Path) -> None:
super(RevealJS, self).open(file) # Override opening with web browser
def convert_to(self, dest: Path) -> None:
"""
Convert this configuration into a zipped RevealJS HTML presentation, saved to
DEST.
"""
with tempfile.TemporaryDirectory() as directory_name:
directory = Path(directory_name)
html_file = directory / dest.with_suffix(".html").name
super().convert_to(html_file)
shutil.make_archive(str(dest.with_suffix("")), "zip", directory_name)
class FrameIndex(str, Enum):
first = "first"
last = "last"
def __repr__(self) -> str:
return self.value
class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0
frame_index: FrameIndex = Field(
FrameIndex.last,
description="What frame (first or last) is used to represent each slide.",
)
resolution: PositiveFloat = Field(
100.0, description="Image resolution use for saving frames."
)
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PDF presentation, saved to DEST."""
images = []
@ -471,17 +721,30 @@ class PDF(Converter):
class PowerPoint(Converter):
left: PositiveInt = 0
top: PositiveInt = 0
width: PositiveInt = 1280
height: PositiveInt = 720
auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None
left: PositiveInt = Field(
0, description="Horizontal offset where the video is placed from left border."
)
top: PositiveInt = Field(
0, description="Vertical offset where the video is placed from top border."
)
width: PositiveInt = Field(
1280,
description="Width of the slides.\nThis should match the resolution of the presentation.",
)
height: PositiveInt = Field(
720,
description="Height of the slides.\nThis should match the resolution of the presentation.",
)
auto_play_media: bool = Field(
True, description="Automatically play animations when changing slide."
)
poster_frame_image: Optional[FilePath] = Field(
None,
description="Optional image to use when animations are not playing.\n"
"By default, the first frame of each animation is used.\nThis is important to avoid blinking effects between slides.",
)
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
@ -498,7 +761,7 @@ class PowerPoint(Converter):
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
el_cnt = xpath(
media.element.getparent().getparent().getparent(),
'.//p:timing//p:video//p:spTgt[@spid="%s"]' % el_id,
f'.//p:timing//p:video//p:spTgt[@spid="{el_id}"]',
)[0]
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
cond.set("delay", "0")
@ -556,22 +819,39 @@ class PowerPoint(Converter):
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-config` option."""
"""Wrap a function to add a '--show-config' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
def callback(ctx: Context, _param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
if "to" in ctx.params:
to = ctx.params["to"]
cls = Converter.from_string(to)
elif "dest" in ctx.params:
dest = Path(ctx.params["dest"])
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = RevealJS
converter = Converter.from_string(to)
if doc := getattr(cls, "__doc__", ""):
click.echo(textwrap.dedent(doc))
for key, field in converter.model_fields.items():
for key, field in cls.model_fields.items():
if field.is_required():
continue
default = field.get_default(call_default_factory=True)
click.echo(f"{key}: {default}")
click.echo(click.style(key, bold=True) + f": {default}")
if description := field.description:
click.secho(textwrap.indent(description, prefix="# "), dim=True)
ctx.exit()
@ -587,18 +867,31 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-template` option."""
"""Wrap a function to add a '--show-template' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
if "to" in ctx.params:
to = ctx.params["to"]
cls = Converter.from_string(to)
elif "dest" in ctx.params:
dest = Path(ctx.params["dest"])
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = RevealJS
template = ctx.params.get("template", None)
converter = Converter.from_string(to)(
presentation_configs=[PresentationConfig()], template=template
)
converter = cls(presentation_configs=[], template=template)
click.echo(converter.load_template())
ctx.exit()
@ -620,7 +913,7 @@ 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(["auto", "html", "pdf", "pptx"], case_sensitive=False),
type=click.Choice(["auto", "html", "pdf", "pptx", "zip"], case_sensitive=False),
metavar="FORMAT",
default="auto",
show_default=True,
@ -632,7 +925,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
is_flag=True,
help="Open the newly created file using the appropriate application.",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
@click.option(
"-c",
"--config",
@ -640,7 +932,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. "
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
"E.g., pass '-cslide_number=true' to display slide numbers.",
)
@click.option(
"--use-template",
@ -648,7 +940,19 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
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``.",
"To echo the default template, use '--show-template'.",
)
@click.option(
"--one-file",
is_flag=True,
help="Embed all local assets (e.g., video files) in the output file. "
"The is a convenient alias to '-cone_file=true'.",
)
@click.option(
"--offline",
is_flag=True,
help="Download any remote content and store it in the assets folder. "
"The is a convenient alias to '-coffline=true'.",
)
@show_template_option
@show_config_options
@ -659,9 +963,10 @@ def convert(
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: dict[str, str],
template: Optional[Path],
offline: bool,
one_file: bool,
) -> None:
"""Convert SCENE(s) into a given format and writes the result in DEST."""
presentation_configs = get_scenes_presentation_config(scenes, folder)
@ -672,13 +977,42 @@ def convert(
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warn(
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = Converter.from_string(to)
if (
one_file
and issubclass(cls, (RevealJS, HtmlZip))
and "one_file" not in config_options
):
config_options["one_file"] = "true"
# Change data_uri to one_file and print a warning if present
if "data_uri" in config_options:
warnings.warn(
"The 'data_uri' configuration option is deprecated and will be "
"removed in the next major version. "
"Use 'one_file' instead.",
DeprecationWarning,
stacklevel=2,
)
config_options["one_file"] = (
config_options["one_file"]
if "one_file" in config_options
else config_options.pop("data_uri")
)
if (
offline
and issubclass(cls, (RevealJS, HtmlZip))
and "offline" not in config_options
):
config_options["offline"] = "true"
converter = cls(
presentation_configs=presentation_configs,
template=template,

View File

@ -86,6 +86,19 @@ A third application is to render scenes from another specific file::
you should probably not include examples that rely on external files, since
relative paths risk to be broken.
.. note::
If you want to skip rendering the slides (e.g., for testing)
you can either set the ``SKIP_MANIM_SLIDES`` environ
variable (to any value) or pass the ``skip-manim-slides``
tag to ``sphinx``:
.. code-block:: bash
sphinx-build -t skip-manim-slides <OTHER_SPHINX_OPTIONS>
# or if you use a Makefile
make html O=-tskip-manim-slides
Options
-------
@ -181,6 +194,7 @@ from __future__ import annotations
import csv
import itertools as it
import os
import re
import shlex
import sys
@ -269,9 +283,9 @@ class ManimSlidesDirective(Directive):
# Rendering is skipped if the tag skip-manim is present,
# or if we are making the pot-files
should_skip = (
"skip-manim-slides"
in self.state.document.settings.env.app.builder.tags.tags
self.state.document.settings.env.app.builder.tags.has("skip-manim-slides")
or self.state.document.settings.env.app.builder.name == "gettext"
or "SKIP_MANIM_SLIDES" in os.environ
)
if should_skip:
node = SkipManimNode()
@ -325,7 +339,7 @@ class ManimSlidesDirective(Directive):
ref_block = ""
if "quality" in self.options:
quality = f'{self.options["quality"]}_quality'
quality = f"{self.options['quality']}_quality"
else:
quality = "example_quality"
frame_rate = QUALITIES[quality]["frame_rate"]
@ -441,7 +455,7 @@ def _write_rendering_stats(scene_name, run_time, file_name):
[
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
scene_name,
"%.3f" % run_time,
f"{run_time:.3f}",
],
)
@ -469,7 +483,7 @@ def _log_rendering_times(*args):
)
for row in group:
print( # noqa: T201
f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}"
f"{' ' * (max_file_length)} {row[2].rjust(7)}s {row[1]}"
)
print("") # noqa: T201

View File

@ -125,7 +125,7 @@ class ManimSlidesMagic(Magics): # type: ignore
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
could look as follows::
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true one_file=true
class MySlide(Slide):
def construct(self):
@ -222,17 +222,29 @@ class ManimSlidesMagic(Magics): # type: ignore
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
if embed: # Embedding implies data-uri
kwargs["data_uri"] = "true"
# If data_uri is set, raise a warning
if "data_uri" in kwargs:
logger.warning(
"'data_uri' configuration option is deprecated and will be removed in a future release. "
"Please use 'one_file' instead."
)
kwargs["one_file"] = (
kwargs["one_file"]
if "one_file" in kwargs
else kwargs.pop("data_uri")
)
if embed: # Embedding implies one_file
kwargs["one_file"] = "true"
# TODO: FIXME
# Seems like files are blocked so date-uri is the only working option...
if kwargs.get("data_uri", "false").lower().strip() == "false":
logger.warn(
"data_uri option is currently automatically enabled, "
# Seems like files are blocked so one_file is the only working option...
if kwargs.get("one_file", "false").lower().strip() == "false":
logger.warning(
"one_file option is currently automatically enabled, "
"because using local video files does not seem to work properly."
)
kwargs["data_uri"] = "true"
kwargs["one_file"] = "true"
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")

View File

@ -1,7 +1,7 @@
import signal
import sys
from pathlib import Path
from typing import List, Optional, Tuple
from typing import Literal, Optional
import click
from click import Context, Parameter
@ -11,23 +11,6 @@ from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
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()
@folder_path_option
@ -50,7 +33,7 @@ def _list_scenes(folder: Path) -> list[str]:
except (
Exception
) as e: # Could not parse this file as a proper presentation config
logger.warn(
logger.warning(
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
)
@ -239,8 +222,14 @@ def start_at_callback(
)
@click.option(
"--hide-info-window",
is_flag=True,
help="Hide info window.",
flag_value="always",
help="Hide info window. By default, hide the info window if there is only one screen.",
)
@click.option(
"--show-info-window",
"hide_info_window",
flag_value="never",
help="Force to show info window.",
)
@click.option(
"--info-window-screen",
@ -248,11 +237,13 @@ def start_at_callback(
metavar="NUMBER",
type=int,
default=None,
help="Put info window on the given screen (a.k.a. display).",
help="Put info window on the given screen (a.k.a. display). "
"If there is more than one screen, it will by default put the info window "
"on a different screen than the main player.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
def present( # noqa: C901
scenes: list[str],
config_path: Path,
folder: Path,
@ -268,7 +259,7 @@ def present(
screen_number: Optional[int],
playback_rate: float,
next_terminates_loop: bool,
hide_info_window: bool,
hide_info_window: Optional[Literal["always", "never"]],
info_window_screen_number: Optional[int],
) -> None:
"""
@ -302,8 +293,6 @@ def present(
if start_at[1]:
start_at_slide_number = start_at[1]
warn_if_non_desirable_pyside6_version()
from qtpy.QtCore import Qt
from qtpy.QtGui import QScreen
@ -313,22 +302,36 @@ def present(
app = qapp()
app.setApplicationName("Manim Slides")
screens = app.screens()
def get_screen(number: int) -> Optional[QScreen]:
try:
return app.screens()[number]
return screens[number]
except IndexError:
logger.error(
f"Invalid screen number {number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
f"allowed values are from 0 to {len(screens) - 1} (incl.)"
)
return None
should_hide_info_window = False
if hide_info_window is None:
should_hide_info_window = len(screens) == 1
elif hide_info_window == "always":
should_hide_info_window = True
if should_hide_info_window and info_window_screen_number is not None:
logger.warning(
f"Ignoring `--info-window-screen` because `--hide-info-window` is set to `{hide_info_window}`."
)
if screen_number is not None:
screen = get_screen(screen_number)
else:
screen = None
if info_window_screen_number is not None:
if info_window_screen_number is not None and not should_hide_info_window:
info_window_screen = get_screen(info_window_screen_number)
else:
info_window_screen = None
@ -352,11 +355,11 @@ def present(
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
hide_info_window=should_hide_info_window,
info_window_screen=info_window_screen,
)
player.show()
player.show(screens)
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.exec())

View File

@ -4,7 +4,7 @@ from typing import Optional
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer, QVideoFrame
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtWidgets import (
QHBoxLayout,
@ -28,7 +28,6 @@ class Info(QWidget): # type: ignore[misc]
def __init__(
self,
*,
full_screen: bool,
aspect_ratio_mode: Qt.AspectRatioMode,
screen: Optional[QScreen],
) -> None:
@ -38,9 +37,6 @@ class Info(QWidget): # type: ignore[misc]
self.setScreen(screen)
self.move(screen.geometry().topLeft())
if full_screen:
self.setWindowState(Qt.WindowFullScreen)
layout = QHBoxLayout()
# Current slide view
@ -226,6 +222,8 @@ class Player(QMainWindow): # type: ignore[misc]
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.frame = QVideoFrame()
self.audio_output = QAudioOutput()
self.video_widget = QVideoWidget()
self.video_sink = self.video_widget.videoSink()
@ -241,15 +239,12 @@ class Player(QMainWindow): # type: ignore[misc]
self.slide_changed.connect(self.slide_changed_callback)
self.info = Info(
full_screen=full_screen,
aspect_ratio_mode=aspect_ratio_mode,
screen=info_window_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.video_sink.videoFrameChanged.connect(self.frame_changed)
self.hide_info_window = hide_info_window
# Connecting key callbacks
@ -319,7 +314,7 @@ class Player(QMainWindow): # type: ignore[misc]
elif -self.presentations_count <= index < 0:
self.__current_presentation_index = index + self.presentations_count
else:
logger.warn(f"Could not set presentation index to {index}.")
logger.warning(f"Could not set presentation index to {index}.")
return
self.presentation_changed.emit()
@ -343,7 +338,7 @@ class Player(QMainWindow): # type: ignore[misc]
elif -self.current_slides_count <= index < 0:
self.__current_slide_index = index + self.current_slides_count
else:
logger.warn(f"Could not set slide index to {index}.")
logger.warning(f"Could not set slide index to {index}.")
return
self.slide_changed.emit()
@ -468,13 +463,13 @@ class Player(QMainWindow): # type: ignore[misc]
def presentation_changed_callback(self) -> None:
index = self.current_presentation_index
count = self.presentations_count
self.info.scene_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.scene_label.setText(f"{index + 1:4d}/{count:4<d}")
@Slot()
def slide_changed_callback(self) -> None:
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_label.setText(f"{index + 1:4d}/{count:4<d}")
self.info.slide_notes.setText(self.current_slide_config.notes)
self.preview_next_slide()
@ -484,11 +479,28 @@ class Player(QMainWindow): # type: ignore[misc]
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()
def show(self) -> None:
def show(self, screens: list[QScreen]) -> None:
"""Screens is necessary to prevent the info window from being shown on the same screen as the main window (especially in full screen mode)."""
super().show()
if not self.hide_info_window:
self.info.show()
if len(screens) > 1 and self.isFullScreen():
self.ensure_different_screens(screens)
if self.isFullScreen():
self.info.showFullScreen()
else:
self.info.show()
if (
len(screens) > 1 and self.info.screen() == self.screen()
): # It is better when Qt assigns the location, but if it fails to, this is a fallback
self.ensure_different_screens(screens)
def ensure_different_screens(self, screens: list[QScreen]) -> None:
target_screen = screens[1] if self.screen() == screens[0] else screens[0]
self.info.setScreen(target_screen)
self.info.move(target_screen.geometry().topLeft())
@Slot()
def close(self) -> None:
@ -515,6 +527,9 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def reverse(self) -> None:
if self.playing_reversed_slide and self.current_slide_index >= 1:
self.current_slide_index -= 1
self.load_reversed_slide()
self.preview_next_slide()
@ -535,8 +550,10 @@ class Player(QMainWindow): # type: ignore[misc]
def full_screen(self) -> None:
if self.windowState() == Qt.WindowFullScreen:
self.setWindowState(Qt.WindowNoState)
self.info.setWindowState(Qt.WindowNoState)
else:
self.setWindowState(Qt.WindowFullScreen)
self.info.setWindowState(Qt.WindowFullScreen)
@Slot()
def hide_mouse(self) -> None:
@ -545,6 +562,34 @@ class Player(QMainWindow): # type: ignore[misc]
else:
self.setCursor(Qt.BlankCursor)
def frame_changed(self, frame: QVideoFrame) -> None:
"""
Slot to handle possibly invalid frames.
This slot cannot be decorated with ``@Slot`` as
the video sinks are handled in different threads.
As of Qt>=6.5.3, the last frame of every video is "flushed",
resulting in a short black screen between each slide.
To avoid this issue, we check every frame, and avoid playing
invalid ones.
References
----------
1. https://github.com/jeertmans/manim-slides/issues/293
2. https://github.com/jeertmans/manim-slides/pull/464
:param frame: The most recent frame.
"""
if frame.isValid():
self.frame = frame
else:
self.video_sink.setVideoFrame(self.frame) # Reuse previous frame
self.info.video_sink.setVideoFrame(self.frame)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close()

View File

@ -1,7 +1,7 @@
"""
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl [OPTIONS] [ARGS]...``.
``manimgl -w [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
@ -48,6 +48,6 @@ def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
if ce and gl:
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
if gl:
subprocess.run([sys.executable, "-m", "manimlib", *args])
subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
else:
subprocess.run([sys.executable, "-m", "manim", "render", *args])

View File

@ -1,7 +1,7 @@
__all__ = [
"API_NAME",
"MANIM",
"MANIMGL",
"API_NAME",
"Slide",
"ThreeDSlide",
]
@ -35,7 +35,7 @@ API: str = os.environ.get(MANIM_API, "manim").lower()
if API not in API_NAMES:
raise ImportError(
f"Specified MANIM_API={API!r} is not in valid options: " f"{API_NAMES}",
f"Specified MANIM_API={API!r} is not in valid options: {API_NAMES}",
)
API_NAME = API_NAMES[API]

View File

@ -3,6 +3,7 @@ from __future__ import annotations
__all__ = ["BaseSlide"]
import platform
import shutil
from abc import abstractmethod
from collections.abc import MutableMapping, Sequence, ValuesView
from pathlib import Path
@ -32,6 +33,10 @@ LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
class BaseSlide:
disable_caching: bool = False
flush_cache: bool = False
skip_reversing: bool = False
def __init__(
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
) -> None:
@ -44,6 +49,7 @@ class BaseSlide:
self._start_animation = 0
self._canvas: MutableMapping[str, Mobject] = {}
self._wait_time_between_slides = 0.0
self._skip_animations = False
@property
@abstractmethod
@ -170,11 +176,23 @@ class BaseSlide:
animations. You must still call :code:`self.add` or
play some animation that introduces each Mobject for
it to appear. The same applies when removing objects.
.. seealso::
:attr:`canvas` for usage examples.
"""
self._canvas.update(objects)
def remove_from_canvas(self, *names: str) -> None:
"""Remove objects from the canvas."""
"""
Remove objects from the canvas.
:param names: The names of objects to remove.
.. seealso::
:attr:`canvas` for usage examples.
"""
for name in names:
self._canvas.pop(name)
@ -186,8 +204,12 @@ class BaseSlide:
@property
def mobjects_without_canvas(self) -> Sequence[Mobject]:
"""
Return the list of objects contained in the scene, minus those present in
Return the list of Mobjects contained in the scene, minus those present in
the canvas.
.. seealso::
:attr:`canvas` for usage examples.
"""
return [
mobject
@ -256,7 +278,7 @@ class BaseSlide:
self._wait_time_between_slides = max(wait_time, 0.0)
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload `self.play` and increment animation count."""
"""Overload 'self.play' and increment animation count."""
super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1
@ -275,36 +297,57 @@ class BaseSlide:
next slide is played. By default, this is the right arrow key.
:param args:
Positional arguments to be passed to
Positional arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
:param skip_animations:
Exclude the next slide from the output.
If `manim` is used, this is also passed to `:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
which will avoid rendering the corresponding animations.
.. seealso::
:meth:`start_skip_animations`
:meth:`stop_skip_animations`
:param loop:
If set, next slide will be looping.
:param auto_next:
If set, next slide will play immediately play the next slide
upon terminating.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
.. warning::
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``.
.. warning::
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``.
.. warning::
Only supported by ``manim-slides present``.
:param notes:
Presenter notes, in Markdown format.
Note that PowerPoint does not support Markdown.
.. note::
PowerPoint does not support Markdown formatting,
so the text will be displayed as is.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html/pptx``.
.. warning::
Only supported by ``manim-slides present``,
``manim-slides convert --to=html`` and
``manim-slides convert --to=pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param kwargs:
Keyword arguments to be passed to
Keyword arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
@ -426,6 +469,9 @@ class BaseSlide:
self._current_slide += 1
if self._skip_animations:
base_slide_config.skip_animations = True
self._base_slide_config = base_slide_config
self._start_animation = self._current_animation
@ -445,11 +491,17 @@ class BaseSlide:
)
)
def _save_slides(self, use_cache: bool = True) -> None:
def _save_slides(
self,
use_cache: bool = True,
flush_cache: bool = False,
skip_reversing: bool = False,
) -> None:
"""
Save slides, optionally using cached files.
Note that cached files only work with Manim.
.. warning:
Caching files only work with Manim.
"""
self._add_last_slide()
@ -458,6 +510,9 @@ class BaseSlide:
scene_name = str(self)
scene_files_folder = files_folder / scene_name
if flush_cache and scene_files_folder.exists():
shutil.rmtree(scene_files_folder)
scene_files_folder.mkdir(parents=True, exist_ok=True)
files: list[Path] = self._partial_movie_files
@ -480,9 +535,16 @@ class BaseSlide:
ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar,
):
if pre_slide_config.skip_animations:
continue
slide_files = files[pre_slide_config.slides_slice]
file = merge_basenames(slide_files)
try:
file = merge_basenames(slide_files)
except ValueError as e:
raise ValueError(
f"Failed to merge basenames of files for slide: {pre_slide_config!r}"
) from e
dst_file = scene_files_folder / file.name
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
@ -492,7 +554,10 @@ class BaseSlide:
# We only reverse video if it was not present
if not use_cache or not rev_file.exists():
reverse_video_file(dst_file, rev_file)
if skip_reversing:
rev_file = dst_file
else:
reverse_video_file(dst_file, rev_file)
slides.append(
SlideConfig.from_pre_slide_config_and_files(
@ -516,6 +581,24 @@ class BaseSlide:
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
)
def start_skip_animations(self) -> None:
"""
Start skipping animations.
This automatically applies ``skip_animations=True``
to all subsequent calls to :meth:`next_slide`.
This is useful when you want to skip animations from multiple slides in a row,
without having to manually set ``skip_animations=True``.
"""
self._skip_animations = True
def stop_skip_animations(self) -> None:
"""
Stop skipping animations.
"""
self._skip_animations = False
def wipe(
self,
*args: Any,

View File

@ -2,6 +2,8 @@ from pathlib import Path
from typing import Any, Optional
from manim import Scene, ThreeDScene, config
from manim.renderer.opengl_renderer import OpenGLRenderer
from manim.utils.color import rgba_to_color
from ..config import BaseSlideConfig
from .base import BaseSlide
@ -11,27 +13,64 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
"""
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
for slides rendering.
:param args: Positional arguments passed to scene object.
:param output_folder: Where the slide animation files should be written.
:param kwargs: Keyword arguments passed to scene object.
:cvar bool disable_caching: :data:`False`: Whether to disable the use of
cached animation files.
:cvar bool flush_cache: :data:`False`: Whether to flush the cache.
Unlike with Manim, flushing is performed before rendering.
:cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations.
If set to :data:`False`, and no cached reversed animation
exists (or caching is disabled) for a given slide,
then the reversed animation will be simply the same
as the original one, i.e., ``rev_file = file``,
for the current slide config.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
# OpenGL renderer disables 'write_to_movie' by default
# which is required for saving the animations
config["write_to_movie"] = True
super().__init__(*args, **kwargs)
@property
def _frame_shape(self) -> tuple[float, float]:
if isinstance(self.renderer, OpenGLRenderer):
return self.renderer.camera.frame_shape # type: ignore
else:
return (
self.renderer.camera.frame_height,
self.renderer.camera.frame_width,
)
@property
def _frame_height(self) -> float:
return config["frame_height"] # type: ignore
return self._frame_shape[0]
@property
def _frame_width(self) -> float:
return config["frame_width"] # type: ignore
return self._frame_shape[1]
@property
def _background_color(self) -> str:
color = self.camera.background_color
if hex_color := getattr(color, "hex", None):
return hex_color # type: ignore
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
return color.to_hex() # type: ignore
if isinstance(self.renderer, OpenGLRenderer):
return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore
else:
return self.renderer.camera.background_color.to_hex() # type: ignore
@property
def _resolution(self) -> tuple[int, int]:
return config["pixel_width"], config["pixel_height"]
if isinstance(self.renderer, OpenGLRenderer):
return self.renderer.get_pixel_shape() # type: ignore
else:
return (
self.renderer.camera.pixel_width,
self.renderer.camera.pixel_height,
)
@property
def _partial_movie_files(self) -> list[Path]:
@ -56,6 +95,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
def _start_at_animation_number(self) -> Optional[int]:
return config["from_animation_number"] # type: ignore
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload 'self.play' and increment animation count."""
super().play(*args, **kwargs)
if self._base_slide_config.skip_animations:
# Manim will not render the animations, so we reset the animation
# counter to the previous value
self._current_animation -= 1
def next_section(self, *args: Any, **kwargs: Any) -> None:
"""
Alias to :meth:`next_slide`.
@ -78,23 +126,38 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
base_slide_config: BaseSlideConfig,
**kwargs: Any,
) -> None:
Scene.next_section(self, *args, **kwargs)
Scene.next_section(
self, *args, skip_animations=base_slide_config.skip_animations, **kwargs
)
BaseSlide.next_slide.__wrapped__(
self,
base_slide_config=base_slide_config,
)
def render(self, *args: Any, **kwargs: Any) -> None:
"""MANIM render."""
"""MANIM renderer."""
# We need to disable the caching limit since we rely on intermediate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
flush_manim_cache = config["flush_cache"]
if flush_manim_cache:
# We need to postpone flushing *after* we saved slides
config["flush_cache"] = False
super().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
self._save_slides()
self._save_slides(
use_cache=not (config["disable_caching"] or self.disable_caching),
flush_cache=(config["flush_cache"] or self.flush_cache),
skip_reversing=self.skip_reversing,
)
if flush_manim_cache:
self.renderer.file_writer.flush_cache_directory()
class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]

View File

@ -2,7 +2,6 @@ from pathlib import Path
from typing import Any, ClassVar, Optional
from manimlib import Scene, ThreeDCamera
from manimlib.utils.file_ops import get_sorted_integer_files
from .base import BaseSlide
@ -10,42 +9,40 @@ from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs.setdefault("file_writer_config", {}).update(
skip_animations=True,
break_into_partial_movies=True,
write_to_movie=True,
subdivide_output=True,
)
kwargs["preview"] = False # Avoid opening a preview window
super().__init__(*args, **kwargs)
@property
def _frame_height(self) -> float:
return self.camera.frame.get_height() # type: ignore
return float(self.camera.get_frame_height())
@property
def _frame_width(self) -> float:
return self.camera.frame.get_width() # type: ignore
return float(self.camera.get_frame_width())
@property
def _background_color(self) -> str:
return self.camera_config["background_color"].hex # type: ignore
rgba = self.camera.background_rgba
r = int(255 * rgba[0])
g = int(255 * rgba[1])
b = int(255 * rgba[2])
if rgba[3] == 1.0:
return f"#{r:02x}{g:02x}{b:02x}"
a = int(255 * rgba[3])
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
@property
def _resolution(self) -> tuple[int, int]:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
@property
def _partial_movie_files(self) -> list[Path]:
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return [
Path(file)
for file in get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
]
partial_movie_directory = self.file_writer.partial_movie_directory
extension = self.file_writer.movie_file_extension
return sorted(partial_movie_directory.glob(f"*{extension}"))
@property
def _show_progress_bar(self) -> bool:
@ -62,7 +59,11 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer."""
super().run(*args, **kwargs)
self._save_slides(use_cache=False)
self._save_slides(
use_cache=False,
flush_cache=self.flush_cache,
skip_reversing=self.skip_reversing,
)
class ThreeDSlide(Slide):

View File

@ -19,31 +19,32 @@
<body>
<div class="reveal">
<div class="slides">
{%- for presentation_config in presentation_configs -%}
{% for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{%- if one_file -%}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% set file = assets_dir / slide_config.file.name %}
{% set file = assets_dir / (prefix(outer_loop.index0) + 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>
<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>
@ -128,6 +129,9 @@
// Flags if we should monitor the hash and change slides accordingly
respondToHashChanges: {{ respond_to_hash_changes }},
// Enable support for jump-to-slide navigation shortcuts
jumpToSlide: {{ jump_to_slide }},
// Push each slide change to the browser history. Implies `hash: true`
history: {{ history }},
@ -316,27 +320,48 @@
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 one_file -%}
// Fix found by @t-fritsch and @Rapsssito on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
function setVideoBase64(video) {
const sources = video.querySelectorAll('source');
// Update the source of the video
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}`);
}
});
function fixBase64VideoBackground(event) {
// Analyze all slides backgrounds
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
// Get the slide video and its sources for each background
const video = slide.querySelector('video');
if (video) {
setVideoBase64(video);
} else {
// Listen to the creation of the video element
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
if (addedNode.tagName === 'VIDEO') {
setVideoBase64(addedNode);
observer.disconnect(); // Stop observing once the video is handled
}
}
}
}
});
observer.observe(slide, { childList: true, subtree: true });
}
}
}
// Setup base64 videos
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{%- endif %}
</script>

View File

@ -19,7 +19,7 @@ def concatenate_video_files(files: list[Path], dest: Path) -> None:
if len(container.streams.video) > 0:
yield file
else:
logger.warn(
logger.warning(
f"Skipping video file {file} because it does "
"not contain any video stream. "
"This is probably caused by Manim, see: "

View File

@ -63,7 +63,7 @@ class Wizard(QWidget): # type: ignore
self.layout = QGridLayout()
for i, (key, value) in enumerate(self.config.keys.dict().items()):
for i, (key, value) in enumerate(self.config.keys.model_dump().items()):
# Create label for key name information
label = QLabel()
key_info = value["name"] or key
@ -97,7 +97,7 @@ class Wizard(QWidget): # type: ignore
def save_config(self) -> None:
try:
Config.model_validate(self.config.dict())
Config.model_validate(self.config.model_dump())
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)

View File

@ -1,6 +1,6 @@
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
requires = ["hatchling", "hatch-fancy-pypi-readme"]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
@ -17,7 +17,8 @@ classifiers = [
"Topic :: Scientific/Engineering",
]
dependencies = [
"av>=9.0.0",
"av>=9.0.0,<14",
"beautifulsoup4>=4.12.3",
"click>=8.1.3",
"click-default-group>=1.2.2",
"jinja2>=3.1.2",
@ -30,26 +31,28 @@ dependencies = [
"qtpy>=2.4.1",
"requests>=2.28.1",
"rich>=13.3.2",
"rtoml>=0.9.0",
"rtoml>=0.11.0",
"tqdm>=4.64.1",
]
description = "Tool for live presentations using manim"
dynamic = ["version"]
dynamic = ["readme", "version"]
keywords = ["manim", "slides", "plugin", "manimgl"]
license = {text = "MIT"}
license = "MIT"
name = "manim-slides"
readme = "README.md"
requires-python = ">=3.9"
[project.optional-dependencies]
docs = [
"manim-slides[magic,sphinx-directive]",
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"pygments<2.19", # See: https://github.com/ManimCommunity/manim/issues/4104
"sphinx>=7.0.1",
"sphinxcontrib-programoutput>=0.18",
"sphinx-design>=0.6.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
@ -57,17 +60,22 @@ docs = [
full = [
"manim-slides[magic,manim,sphinx-directive]",
]
full-gl = [
"manim-slides[magic,manimgl,sphinx-directive]",
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
pyqt6 = ["pyqt6>=6.6.1"]
manim = ["manim>=0.19"]
manimgl = ["manimgl>=1.7.2"]
pyqt6 = ["pyqt6>=6.7.0"]
pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-missing-modules>=0.1.0",
"pytest-qt>=4.2.0",
]
[project.scripts]
manim-slides = "manim_slides.__main__:cli"
@ -79,11 +87,61 @@ Founding = "https://github.com/sponsors/jeertmans"
Homepage = "https://github.com/jeertmans/manim-slides"
Repository = "https://github.com/jeertmans/manim-slides"
[tool.bumpversion]
allow_dirty = false
commit = true
commit_args = ""
current_version = "5.4.1"
ignore_missing_version = false
message = "chore(deps): bump version from {current_version} to {new_version}"
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
regex = false
replace = "{new_version}"
search = "{current_version}"
serialize = ["{major}.{minor}.{patch}-rc{release}", "{major}.{minor}.{patch}"]
sign_tags = false
tag = false
tag_message = "Bump version: {current_version} → {new_version}"
tag_name = "v{new_version}"
[[tool.bumpversion.files]]
filename = "manim_slides/__version__.py"
replace = '__version__ = "{new_version}"'
search = '__version__ = "{current_version}"'
[[tool.bumpversion.files]]
filename = "CITATION.cff"
replace = "version: v{new_version}"
search = "version: v{current_version}"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = "v{new_version}"
search = "Unreleased"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = "v{new_version}"
search = "unreleased"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = "v{current_version}...v{new_version}"
search = "v{current_version}...HEAD"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = '''<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)'''
search = "<!-- start changelog -->"
[tool.codespell]
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
check-hidden = true
ignore-words-list = "master"
skip = "requirements.lock,requirements-dev.lock"
skip = "uv.lock"
[tool.coverage.report]
exclude_lines = [
@ -94,6 +152,25 @@ exclude_lines = [
]
precision = 2
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
text = """<p align="center">
<a href="https://www.github.com/jeertmans/manin-slides">
<img src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png"/>
</a>
</p>
"""
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "README.md"
start-after = "<!-- start pypi -->"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '> \[!([A-Z]+)\]'
replacement = '> **\1:**'
[tool.hatch.version]
path = "manim_slides/__version__.py"
@ -104,12 +181,20 @@ python_version = "3.9"
strict = true
[tool.pytest.ini_options]
addopts = [
"--cov-report=xml",
"--cov=manim_slides",
]
env = [
"QT_API=pyside6",
"QT_QPA_PLATFORM=offscreen",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
'''ignore:'audioop' is deprecated:DeprecationWarning''',
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
'ignore::DeprecationWarning:pkg_resources.*:',
'ignore:invalid escape sequence.*:DeprecationWarning',
'ignore:invalid escape sequence.*:SyntaxWarning',
]
[tool.ruff]
@ -136,15 +221,13 @@ extend-ignore = [
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ["manim_slides", "tests"]}
[tool.rye]
[tool.ruff.lint.per-file-ignores]
"docs/source/reference/magic_example.ipynb" = ["F403", "F405"]
"tests/test_slide.py" = ["N801"]
[tool.uv]
dev-dependencies = [
"bump2version>=1.0.1",
"manim-slides[manim,manimgl,pyqt6]",
"bump-my-version>=0.20.3",
"pre-commit>=3.5.0",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
"pytest-xdist>=3.3.1",
"setuptools>=73.0.1",
]
managed = true

View File

@ -1,419 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: true
# with-sources: false
-e file:.
alabaster==0.7.16
# via sphinx
annotated-types==0.6.0
# via pydantic
asttokens==2.4.1
# via stack-data
attrs==23.2.0
# via jsonschema
# via referencing
av==12.0.0
# via manim-slides
babel==2.14.0
# via sphinx
beautifulsoup4==4.12.3
# via furo
# via nbconvert
bleach==6.1.0
# via nbconvert
bump2version==1.0.1
certifi==2024.2.2
# via requests
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via click-default-group
# via cloup
# via manim
# via manim-slides
# via sphinx-click
click-default-group==1.2.4
# via manim
# via manim-slides
cloup==0.13.1
# via manim
colour==0.1.5
# via manim
# via manimgl
comm==0.2.2
# via ipykernel
contourpy==1.2.1
# via matplotlib
coverage==7.4.4
# via pytest-cov
cycler==0.12.1
# via matplotlib
debugpy==1.8.1
# via ipykernel
decorator==5.1.1
# via ipython
# via manim
defusedxml==0.7.1
# via nbconvert
distlib==0.3.8
# via virtualenv
docutils==0.20.1
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx
# via sphinx-click
execnet==2.1.1
# via pytest-xdist
executing==2.0.1
# via stack-data
fastjsonschema==2.19.1
# via nbformat
filelock==3.13.4
# via virtualenv
fonttools==4.51.0
# via matplotlib
furo==2024.1.29
# via manim-slides
glcontext==2.5.0
# via moderngl
identify==2.5.35
# via pre-commit
idna==3.7
# via requests
imagesize==1.4.1
# via sphinx
iniconfig==2.0.0
# via pytest
ipykernel==6.29.4
# via manim-slides
ipython==8.18.1
# via ipykernel
# via manim-slides
# via manimgl
isosurfaces==0.1.0
# via manim
# via manimgl
jedi==0.19.1
# via ipython
jinja2==3.1.3
# via manim-slides
# via myst-parser
# via nbconvert
# via nbsphinx
# via sphinx
jsonschema==4.21.1
# via nbformat
jsonschema-specifications==2023.12.1
# via jsonschema
jupyter-client==8.6.1
# via ipykernel
# via nbclient
jupyter-core==5.7.2
# via ipykernel
# via jupyter-client
# via nbclient
# via nbconvert
# via nbformat
jupyterlab-pygments==0.3.0
# via nbconvert
kiwisolver==1.4.5
# via matplotlib
lxml==5.2.1
# via manim-slides
# via python-pptx
manim==0.17.3
# via manim-slides
manimgl==1.6.1
# via manim-slides
manimpango==0.4.4
# via manim
# via manimgl
mapbox-earcut==1.0.1
# via manim
# via manimgl
markdown-it-py==3.0.0
# via mdit-py-plugins
# via myst-parser
# via rich
markupsafe==2.1.5
# via jinja2
# via nbconvert
matplotlib==3.8.4
# via manimgl
matplotlib-inline==0.1.7
# via ipykernel
# via ipython
mdit-py-plugins==0.4.0
# via myst-parser
mdurl==0.1.2
# via markdown-it-py
mistune==3.0.2
# via nbconvert
moderngl==5.10.0
# via manim
# via manimgl
# via moderngl-window
moderngl-window==2.4.4
# via manim
# via manimgl
mpmath==1.3.0
# via sympy
multipledispatch==1.0.0
# via pyrr
myst-parser==2.0.0
# via manim-slides
nbclient==0.10.0
# via nbconvert
nbconvert==7.16.3
# via nbsphinx
nbformat==5.10.4
# via nbclient
# via nbconvert
# via nbsphinx
nbsphinx==0.9.3
# via manim-slides
nest-asyncio==1.6.0
# via ipykernel
networkx==2.8.8
# via manim
nodeenv==1.8.0
# via pre-commit
numpy==1.26.4
# via contourpy
# via isosurfaces
# via manim
# via manim-slides
# via manimgl
# via mapbox-earcut
# via matplotlib
# via moderngl-window
# via pyrr
# via scipy
packaging==24.0
# via ipykernel
# via matplotlib
# via nbconvert
# via pytest
# via qtpy
# via sphinx
pandoc==2.3
# via manim-slides
pandocfilters==1.5.1
# via nbconvert
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pillow==9.5.0
# via manim
# via manim-slides
# via manimgl
# via matplotlib
# via moderngl-window
# via python-pptx
platformdirs==4.2.0
# via jupyter-core
# via virtualenv
pluggy==1.4.0
# via pytest
# via pytest-qt
plumbum==1.8.2
# via pandoc
ply==3.11
# via pandoc
pre-commit==3.7.0
prompt-toolkit==3.0.43
# via ipython
psutil==5.9.8
# via ipykernel
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.2
# via stack-data
pycairo==1.26.0
# via manim
pydantic==2.7.0
# via manim-slides
# via pydantic-extra-types
pydantic-core==2.18.1
# via pydantic
pydantic-extra-types==2.6.0
# via manim-slides
pydub==0.25.1
# via manim
# via manimgl
pyglet==2.0.15
# via moderngl-window
pygments==2.17.2
# via furo
# via ipython
# via manim
# via manimgl
# via nbconvert
# via rich
# via sphinx
pyopengl==3.1.7
# via manimgl
pyparsing==3.1.2
# via matplotlib
pyqt6==6.6.1
# via manim-slides
pyqt6-qt6==6.6.3
# via pyqt6
pyqt6-sip==13.6.0
# via pyqt6
pyrr==0.10.3
# via moderngl-window
pyside6==6.5.2
# via manim-slides
pyside6-addons==6.5.2
# via pyside6
pyside6-essentials==6.5.2
# via pyside6
# via pyside6-addons
pytest==8.1.1
# via pytest-cov
# via pytest-env
# via pytest-qt
# via pytest-xdist
pytest-cov==5.0.0
pytest-env==1.1.3
pytest-qt==4.4.0
pytest-xdist==3.5.0
python-dateutil==2.9.0.post0
# via jupyter-client
# via matplotlib
python-pptx==0.6.23
# via manim-slides
pyyaml==6.0.1
# via manimgl
# via myst-parser
# via pre-commit
pyzmq==26.0.0
# via ipykernel
# via jupyter-client
qtpy==2.4.1
# via manim-slides
referencing==0.34.0
# via jsonschema
# via jsonschema-specifications
requests==2.31.0
# via manim
# via manim-slides
# via sphinx
rich==13.7.1
# via manim
# via manim-slides
# via manimgl
rpds-py==0.18.0
# via jsonschema
# via referencing
rtoml==0.10.0
# via manim-slides
scipy==1.13.0
# via manim
# via manimgl
screeninfo==0.8.1
# via manim
# via manimgl
setuptools==69.5.1
# via nodeenv
shiboken6==6.5.2
# via pyside6
# via pyside6-addons
# via pyside6-essentials
six==1.16.0
# via asttokens
# via bleach
# via python-dateutil
skia-pathops==0.7.4
# via manim
# via manimgl
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
# via beautifulsoup4
sphinx==7.3.6
# via furo
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx-basic-ng
# via sphinx-click
# via sphinx-copybutton
# via sphinxext-opengraph
sphinx-basic-ng==1.0.0b2
# via furo
sphinx-click==5.1.0
# via manim-slides
sphinx-copybutton==0.5.2
# via manim-slides
sphinxcontrib-applehelp==1.0.8
# via sphinx
sphinxcontrib-devhelp==1.0.6
# via sphinx
sphinxcontrib-htmlhelp==2.0.5
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.7
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
# via sphinx
sphinxext-opengraph==0.9.1
# via manim-slides
srt==3.5.3
# via manim
stack-data==0.6.3
# via ipython
svgelements==1.9.6
# via manim
# via manimgl
sympy==1.12
# via manimgl
tinycss2==1.2.1
# via nbconvert
tornado==6.4
# via ipykernel
# via jupyter-client
tqdm==4.66.2
# via manim
# via manim-slides
# via manimgl
traitlets==5.14.2
# via comm
# via ipykernel
# via ipython
# via jupyter-client
# via jupyter-core
# via matplotlib-inline
# via nbclient
# via nbconvert
# via nbformat
# via nbsphinx
typing-extensions==4.11.0
# via pydantic
# via pydantic-core
urllib3==2.2.1
# via requests
validators==0.28.0
# via manimgl
virtualenv==20.25.3
# via pre-commit
watchdog==2.3.1
# via manim
wcwidth==0.2.13
# via prompt-toolkit
webencodings==0.5.1
# via bleach
# via tinycss2
xlsxwriter==3.2.0
# via python-pptx

View File

@ -1,382 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: true
# with-sources: false
-e file:.
alabaster==0.7.16
# via sphinx
annotated-types==0.6.0
# via pydantic
asttokens==2.4.1
# via stack-data
attrs==23.2.0
# via jsonschema
# via referencing
av==12.0.0
# via manim-slides
babel==2.14.0
# via sphinx
beautifulsoup4==4.12.3
# via furo
# via nbconvert
bleach==6.1.0
# via nbconvert
certifi==2024.2.2
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via click-default-group
# via cloup
# via manim
# via manim-slides
# via sphinx-click
click-default-group==1.2.4
# via manim
# via manim-slides
cloup==0.13.1
# via manim
colour==0.1.5
# via manim
# via manimgl
comm==0.2.2
# via ipykernel
contourpy==1.2.1
# via matplotlib
cycler==0.12.1
# via matplotlib
debugpy==1.8.1
# via ipykernel
decorator==5.1.1
# via ipython
# via manim
defusedxml==0.7.1
# via nbconvert
docutils==0.20.1
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx
# via sphinx-click
executing==2.0.1
# via stack-data
fastjsonschema==2.19.1
# via nbformat
fonttools==4.51.0
# via matplotlib
furo==2024.1.29
# via manim-slides
glcontext==2.5.0
# via moderngl
idna==3.7
# via requests
imagesize==1.4.1
# via sphinx
ipykernel==6.29.4
# via manim-slides
ipython==8.18.1
# via ipykernel
# via manim-slides
# via manimgl
isosurfaces==0.1.0
# via manim
# via manimgl
jedi==0.19.1
# via ipython
jinja2==3.1.3
# via manim-slides
# via myst-parser
# via nbconvert
# via nbsphinx
# via sphinx
jsonschema==4.21.1
# via nbformat
jsonschema-specifications==2023.12.1
# via jsonschema
jupyter-client==8.6.1
# via ipykernel
# via nbclient
jupyter-core==5.7.2
# via ipykernel
# via jupyter-client
# via nbclient
# via nbconvert
# via nbformat
jupyterlab-pygments==0.3.0
# via nbconvert
kiwisolver==1.4.5
# via matplotlib
lxml==5.2.1
# via manim-slides
# via python-pptx
manim==0.17.3
# via manim-slides
manimgl==1.6.1
# via manim-slides
manimpango==0.4.4
# via manim
# via manimgl
mapbox-earcut==1.0.1
# via manim
# via manimgl
markdown-it-py==3.0.0
# via mdit-py-plugins
# via myst-parser
# via rich
markupsafe==2.1.5
# via jinja2
# via nbconvert
matplotlib==3.8.4
# via manimgl
matplotlib-inline==0.1.7
# via ipykernel
# via ipython
mdit-py-plugins==0.4.0
# via myst-parser
mdurl==0.1.2
# via markdown-it-py
mistune==3.0.2
# via nbconvert
moderngl==5.10.0
# via manim
# via manimgl
# via moderngl-window
moderngl-window==2.4.4
# via manim
# via manimgl
mpmath==1.3.0
# via sympy
multipledispatch==1.0.0
# via pyrr
myst-parser==2.0.0
# via manim-slides
nbclient==0.10.0
# via nbconvert
nbconvert==7.16.3
# via nbsphinx
nbformat==5.10.4
# via nbclient
# via nbconvert
# via nbsphinx
nbsphinx==0.9.3
# via manim-slides
nest-asyncio==1.6.0
# via ipykernel
networkx==2.8.8
# via manim
numpy==1.26.4
# via contourpy
# via isosurfaces
# via manim
# via manim-slides
# via manimgl
# via mapbox-earcut
# via matplotlib
# via moderngl-window
# via pyrr
# via scipy
packaging==24.0
# via ipykernel
# via matplotlib
# via nbconvert
# via qtpy
# via sphinx
pandoc==2.3
# via manim-slides
pandocfilters==1.5.1
# via nbconvert
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pillow==9.5.0
# via manim
# via manim-slides
# via manimgl
# via matplotlib
# via moderngl-window
# via python-pptx
platformdirs==4.2.0
# via jupyter-core
plumbum==1.8.2
# via pandoc
ply==3.11
# via pandoc
prompt-toolkit==3.0.43
# via ipython
psutil==5.9.8
# via ipykernel
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.2
# via stack-data
pycairo==1.26.0
# via manim
pydantic==2.7.0
# via manim-slides
# via pydantic-extra-types
pydantic-core==2.18.1
# via pydantic
pydantic-extra-types==2.6.0
# via manim-slides
pydub==0.25.1
# via manim
# via manimgl
pyglet==2.0.15
# via moderngl-window
pygments==2.17.2
# via furo
# via ipython
# via manim
# via manimgl
# via nbconvert
# via rich
# via sphinx
pyopengl==3.1.7
# via manimgl
pyparsing==3.1.2
# via matplotlib
pyqt6==6.6.1
# via manim-slides
pyqt6-qt6==6.6.3
# via pyqt6
pyqt6-sip==13.6.0
# via pyqt6
pyrr==0.10.3
# via moderngl-window
pyside6==6.5.2
# via manim-slides
pyside6-addons==6.5.2
# via pyside6
pyside6-essentials==6.5.2
# via pyside6
# via pyside6-addons
python-dateutil==2.9.0.post0
# via jupyter-client
# via matplotlib
python-pptx==0.6.23
# via manim-slides
pyyaml==6.0.1
# via manimgl
# via myst-parser
pyzmq==26.0.0
# via ipykernel
# via jupyter-client
qtpy==2.4.1
# via manim-slides
referencing==0.34.0
# via jsonschema
# via jsonschema-specifications
requests==2.31.0
# via manim
# via manim-slides
# via sphinx
rich==13.7.1
# via manim
# via manim-slides
# via manimgl
rpds-py==0.18.0
# via jsonschema
# via referencing
rtoml==0.10.0
# via manim-slides
scipy==1.13.0
# via manim
# via manimgl
screeninfo==0.8.1
# via manim
# via manimgl
shiboken6==6.5.2
# via pyside6
# via pyside6-addons
# via pyside6-essentials
six==1.16.0
# via asttokens
# via bleach
# via python-dateutil
skia-pathops==0.7.4
# via manim
# via manimgl
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
# via beautifulsoup4
sphinx==7.3.6
# via furo
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx-basic-ng
# via sphinx-click
# via sphinx-copybutton
# via sphinxext-opengraph
sphinx-basic-ng==1.0.0b2
# via furo
sphinx-click==5.1.0
# via manim-slides
sphinx-copybutton==0.5.2
# via manim-slides
sphinxcontrib-applehelp==1.0.8
# via sphinx
sphinxcontrib-devhelp==1.0.6
# via sphinx
sphinxcontrib-htmlhelp==2.0.5
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.7
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
# via sphinx
sphinxext-opengraph==0.9.1
# via manim-slides
srt==3.5.3
# via manim
stack-data==0.6.3
# via ipython
svgelements==1.9.6
# via manim
# via manimgl
sympy==1.12
# via manimgl
tinycss2==1.2.1
# via nbconvert
tornado==6.4
# via ipykernel
# via jupyter-client
tqdm==4.66.2
# via manim
# via manim-slides
# via manimgl
traitlets==5.14.2
# via comm
# via ipykernel
# via ipython
# via jupyter-client
# via jupyter-core
# via matplotlib-inline
# via nbclient
# via nbconvert
# via nbformat
# via nbsphinx
typing-extensions==4.11.0
# via pydantic
# via pydantic-core
urllib3==2.2.1
# via requests
validators==0.28.0
# via manimgl
watchdog==2.3.1
# via manim
wcwidth==0.2.13
# via prompt-toolkit
webencodings==0.5.1
# via bleach
# via tinycss2
xlsxwriter==3.2.0
# via python-pptx

View File

@ -1,5 +1,5 @@
#! /bin/bash
rye run manim-slides render -t -qk -s --format png --resolution 64,64 static/logo.py ManimSlidesFavicon && mv media/images/logo/*.png static/favicon.png
uv run manim-slides render -t -qk -s --format png --resolution 64,64 static/logo.py ManimSlidesFavicon && mv media/images/logo/*.png static/favicon.png
ln -f -r -s static/favicon.png docs/source/_static/favicon.png

View File

@ -1,21 +1,21 @@
#! /bin/bash
MANIM_SLIDES_THEME=light rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo.png
MANIM_SLIDES_THEME=light uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && 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 rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_docs.png
MANIM_SLIDES_THEME=dark_docs uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && 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 rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_github.png
MANIM_SLIDES_THEME=dark_github uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && 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 rye run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_light_transparent.png
MANIM_SLIDES_THEME=light uv run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && 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 rye run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_transparent.png
MANIM_SLIDES_THEME=dark_docs uv run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && 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

@ -71,8 +71,20 @@ def paths() -> Generator[list[Path], None, None]:
yield [random_path() for _ in range(20)]
@pytest.fixture(scope="session")
@pytest.fixture
def presentation_config(
slides_folder: Path,
) -> Generator[PresentationConfig, None, None]:
yield PresentationConfig.from_file(slides_folder / "BasicSlide.json")
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
"""Make sure missing modules run at the very end."""
def uses_missing_modules_fixtures(item: pytest.Item) -> int:
if "missing_modules" in getattr(item, "fixturenames", []):
return 1
return 0
items.sort(key=uses_missing_modules_fixtures)

View File

@ -38,3 +38,7 @@ class BasicSlide(Slide):
self.next_slide()
self.zoom(other_text, [])
class BasicSlideSkipReversing(BasicSlide):
skip_reversing = True

View File

@ -86,6 +86,15 @@ class TestBaseSlide:
assert base_slide.wait_time_between_slides == 0.0
def test_skip_animations(self, base_slide: BaseSlide) -> None:
assert base_slide._skip_animations == False
def test_start_and_stop_skip_animations(self, base_slide: BaseSlide) -> None:
base_slide.start_skip_animations()
assert base_slide._skip_animations == True
base_slide.stop_skip_animations()
assert base_slide._skip_animations == False
def test_play(self) -> None:
pass # This method should be tested in test_slide.py

73
tests/test_checkhealth.py Normal file
View File

@ -0,0 +1,73 @@
import importlib.util
import sys
from itertools import chain, combinations
import pytest
from click.testing import CliRunner
from pytest_missing_modules.plugin import MissingModulesContextGenerator
from manim_slides.__version__ import __version__
from manim_slides.checkhealth import checkhealth
MANIM_NOT_INSTALLED = importlib.util.find_spec("manim") is None
MANIMGL_NOT_INSTALLED = importlib.util.find_spec("manimlib") is None
PYQT6_NOT_INSTALLED = importlib.util.find_spec("PyQt6") is None
PYSIDE6_NOT_INSTALLED = importlib.util.find_spec("PySide6") is None
@pytest.mark.filterwarnings("ignore:Selected binding 'pyqt6' could not be found")
@pytest.mark.parametrize(
"names",
list(
chain.from_iterable(
combinations(("manim", "manimlib", "PyQt6", "PySide6"), r=r)
for r in range(0, 5)
)
),
)
def test_checkhealth(
names: tuple[str, ...], missing_modules: MissingModulesContextGenerator
) -> None:
runner = CliRunner()
manim_missing = "manim" in names or MANIM_NOT_INSTALLED
manimlib_missing = "manimlib" in names or MANIMGL_NOT_INSTALLED
pyqt6_missing = "PyQt6" in names or PYQT6_NOT_INSTALLED
pyside6_missing = "PySide6" in names or PYSIDE6_NOT_INSTALLED
if "qtpy" in sys.modules:
del sys.modules["qtpy"] # Avoid using cached module
with missing_modules(*names):
if (
not manimlib_missing
and not MANIMGL_NOT_INSTALLED
and sys.version_info < (3, 10)
):
pytest.skip("See https://github.com/3b1b/manim/issues/2263")
result = runner.invoke(
checkhealth,
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
)
assert result.exit_code == 0
assert f"Manim Slides version: {__version__}" in result.output
assert sys.executable in result.output
if manim_missing:
assert "manim not found" in result.output
else:
assert "manim (version:" in result.output
if manimlib_missing:
assert "manimgl not found" in result.output
else:
assert "manimgl (version:" in result.output
if pyqt6_missing and pyside6_missing:
assert "No Qt API found" in result.output
elif pyqt6_missing:
assert "Qt API: pyside6 (version:" in result.output
else:
assert "Qt API: pyqt6 (version:" in result.output

View File

@ -1,8 +1,10 @@
import shutil
from enum import EnumMeta
from pathlib import Path
import pytest
from pydantic import ValidationError
import requests
from bs4 import BeautifulSoup
from manim_slides.config import PresentationConfig
from manim_slides.convert import (
@ -17,6 +19,7 @@ from manim_slides.convert import (
ControlsLayout,
Converter,
Display,
HtmlZip,
JsBool,
JsFalse,
JsNull,
@ -138,7 +141,8 @@ def test_unquoted_enum(enum_type: EnumMeta) -> None:
class TestConverter:
@pytest.mark.parametrize(
("name", "converter"), [("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint)]
("name", "converter"),
[("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint), ("zip", HtmlZip)],
)
def test_from_string(self, name: str, converter: type) -> None:
assert Converter.from_string(name) == converter
@ -150,9 +154,164 @@ class TestConverter:
RevealJS(presentation_configs=[presentation_config]).convert_to(out_file)
assert out_file.exists()
assert Path(tmp_path / "slides_assets").is_dir()
file_contents = Path(out_file).read_text()
file_contents = out_file.read_text()
assert "manim" in file_contents.casefold()
def test_revealjs_offline_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
out_file = tmp_path / "slides.html"
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
out_file
)
assert out_file.exists()
assets_dir = Path(tmp_path / "slides_assets")
assert assets_dir.is_dir()
for file in [
"black.min.css",
"reveal.min.css",
"reveal.min.js",
"zenburn.min.css",
]:
assert (assets_dir / file).exists()
def test_revealjs_data_encode(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Mock requests.Session.get to return a fake response (should not be called)
class MockResponse:
def __init__(self, content: bytes, text: str, status_code: int) -> None:
self.content = content
self.text = text
self.status_code = status_code
# Apply the monkeypatch
monkeypatch.setattr(
requests.Session,
"get",
lambda self, url: MockResponse(
b"body { background-color: #9a3241; }",
"body { background-color: #9a3241; }",
200,
),
)
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[presentation_config], offline="false", one_file="true"
).convert_to(out_file)
assert out_file.exists()
# Check that assets are not stored
assert not (tmp_path / "slides_assets").exists()
with open(out_file, encoding="utf-8") as file:
content = file.read()
soup = BeautifulSoup(content, "html.parser")
# Check if video is encoded in base64
videos = soup.find_all("section")
assert all(
"data:video/mp4;base64," in video["data-background-video"]
for video in videos
)
# Check if CSS is not inlined
styles = soup.find_all("style")
assert not any("background-color: #9a3241;" in style.string for style in styles)
# Check if JS is not inlined
scripts = soup.find_all("script")
assert not any(
"background-color: #9a3241;" in (script.string or "") for script in scripts
)
def test_revealjs_offline_inlining(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Mock requests.Session.get to return a fake response
class MockResponse:
def __init__(self, content: bytes, text: str, status_code: int) -> None:
self.content = content
self.text = text
self.status_code = status_code
# Apply the monkeypatch
monkeypatch.setattr(
requests.Session,
"get",
lambda self, url: MockResponse(
b"body { background-color: #9a3241; }",
"body { background-color: #9a3241; }",
200,
),
)
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[presentation_config], offline="true", one_file="true"
).convert_to(out_file)
assert out_file.exists()
with open(out_file, encoding="utf-8") as file:
content = file.read()
soup = BeautifulSoup(content, "html.parser")
# Check if CSS is inlined
styles = soup.find_all("style")
assert any("background-color: #9a3241;" in style.string for style in styles)
# Check if JS is inlined
scripts = soup.find_all("script")
assert any("background-color: #9a3241;" in script.string for script in scripts)
def test_htmlzip_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
archive = tmp_path / "got.zip"
expected = tmp_path / "expected.html"
got = tmp_path / "got.html"
HtmlZip(presentation_configs=[presentation_config]).convert_to(archive)
RevealJS(presentation_configs=[presentation_config]).convert_to(expected)
shutil.unpack_archive(str(archive), extract_dir=tmp_path)
assert archive.exists()
assert got.exists()
assert expected.exists()
assert got.read_text() == expected.read_text().replace(
"expected_assets", "got_assets"
)
@pytest.mark.parametrize("num_presentation_configs", (1, 2))
def test_revealjs_multiple_scenes_converter(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
num_presentation_configs: int,
) -> None:
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[
presentation_config for _ in range(num_presentation_configs)
]
).convert_to(out_file)
assert out_file.exists()
assets_dir = Path(tmp_path / "slides_assets")
assert assets_dir.is_dir()
got = sum(1 for _ in assets_dir.iterdir())
expected = num_presentation_configs * len(presentation_config.slides)
assert got == expected
@pytest.mark.parametrize("frame_index", ("first", "last"))
def test_pdf_converter(
self, frame_index: str, tmp_path: Path, presentation_config: PresentationConfig
@ -163,10 +322,6 @@ class TestConverter:
).convert_to(out_file)
assert out_file.exists()
def test_converter_no_presentation_config(self) -> None:
with pytest.raises(ValidationError):
Converter(presentation_configs=[])
def test_pptx_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:

View File

@ -1,3 +1,4 @@
import warnings
from pathlib import Path
import pytest
@ -64,6 +65,34 @@ def test_convert(slides_folder: Path, extension: str) -> None:
assert results.exit_code == 0
@pytest.mark.parametrize(("extension",), [("html",)])
def test_convert_data_uri_deprecated(slides_folder: Path, extension: str) -> None:
runner = CliRunner(mix_stderr=False)
with runner.isolated_filesystem():
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
results = runner.invoke(
cli,
[
"convert",
"BasicSlide",
f"basic_example.{extension}",
"--folder",
str(slides_folder),
"--to",
extension,
"-cdata_uri=true",
],
)
assert any(
"'data_uri' configuration option is deprecated" in str(item.message)
and item.category is DeprecationWarning
for item in w
)
assert results.exit_code == 0
@pytest.mark.parametrize(
("extension", "expected_log"),
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],

View File

@ -6,6 +6,11 @@ import pytest
import manim_slides.slide as slide
skip_if_py39 = pytest.mark.skipif(
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263",
)
def assert_import(
*,
@ -20,6 +25,7 @@ def assert_import(
assert slide.MANIMGL == manimgl
@skip_if_py39
def test_force_api() -> None:
pytest.importorskip("manimlib")
import manim # noqa: F401
@ -53,6 +59,7 @@ def test_invalid_api() -> None:
del os.environ[slide.MANIM_API]
@skip_if_py39
@pytest.mark.filterwarnings("ignore:assert_import")
def test_manim_and_manimgl_imported() -> None:
pytest.importorskip("manimlib")
@ -79,6 +86,7 @@ def test_manim_imported() -> None:
)
@skip_if_py39
def test_manimgl_imported() -> None:
pytest.importorskip("manimlib")
import manimlib # noqa: F401

View File

@ -1,14 +1,16 @@
import contextlib
import os
import random
import shutil
import sys
import tempfile
from collections.abc import Iterator
from pathlib import Path
from typing import Any, Union
import manim
import numpy as np
import pytest
from click.testing import CliRunner
from manim import (
BLACK,
BLUE,
DOWN,
LEFT,
@ -19,15 +21,42 @@ from manim import (
Dot,
FadeIn,
GrowFromCenter,
Square,
Text,
)
from packaging import version
from pydantic import ValidationError
from manim.renderer.opengl_renderer import OpenGLRenderer
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
from manim_slides.slide.manim import Slide as CESlide
if sys.version_info < (3, 10):
class _GLSlide:
def construct(self) -> None:
pass
def render(self) -> None:
pass
GLSlide = pytest.param(
_GLSlide,
marks=pytest.mark.skip(reason="See https://github.com/3b1b/manim/issues/2263"),
)
else:
from manim_slides.slide.manimlib import Slide as GLSlide
_GLSlide = GLSlide
class CEGLSlide(CESlide):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
SlideType = Union[type[CESlide], type[_GLSlide], type[CEGLSlide]]
Slide = Union[CESlide, _GLSlide, CEGLSlide]
@pytest.mark.parametrize(
@ -37,12 +66,13 @@ from manim_slides.slide.manim import Slide
pytest.param(
"--GL",
marks=pytest.mark.skipif(
version.parse(np.__version__) >= version.parse("1.25")
or sys.version_info >= (3, 12),
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
"--CE --renderer=opengl",
],
ids=("CE", "GL", "CE(GL)"),
)
def test_render_basic_slide(
renderer: str,
@ -55,7 +85,7 @@ def test_render_basic_slide(
with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke(
render, [renderer, str(slides_file), "BasicSlide", "-ql"]
render, [*renderer.split(" "), str(slides_file), "BasicSlide", "-ql"]
)
assert results.exit_code == 0, results
@ -83,272 +113,497 @@ def test_render_basic_slide(
assert local_presentation_config.resolution == presentation_config.resolution
def assert_constructs(cls: type) -> type:
class Wrapper:
@classmethod
def test_construct(_) -> None: # noqa: N804
cls().construct()
def test_clear_cache(
slides_file: Path,
) -> None:
runner = CliRunner()
return Wrapper
with runner.isolated_filesystem() as tmp_dir:
local_media_folder = (
Path(tmp_dir)
/ "media"
/ "videos"
/ slides_file.stem
/ "480p15"
/ "partial_movie_files"
/ "BasicSlide"
)
local_slides_folder = Path(tmp_dir) / "slides"
assert not local_media_folder.exists()
assert not local_slides_folder.exists()
results = runner.invoke(render, [str(slides_file), "BasicSlide", "-ql"])
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and list(local_media_folder.iterdir())
assert local_slides_folder.exists()
results = runner.invoke(
render, [str(slides_file), "BasicSlide", "-ql", "--flush_cache"]
)
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and not list(local_media_folder.iterdir())
assert local_slides_folder.exists()
results = runner.invoke(
render, [str(slides_file), "BasicSlide", "-ql", "--disable_caching"]
)
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and list(local_media_folder.iterdir())
assert local_slides_folder.exists()
results = runner.invoke(
render,
[
str(slides_file),
"BasicSlide",
"-ql",
"--disable_caching",
"--flush_cache",
],
)
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and not list(local_media_folder.iterdir())
assert local_slides_folder.exists()
def assert_renders(cls: type) -> type:
class Wrapper:
@classmethod
def test_render(_) -> None: # noqa: N804
cls().render()
@pytest.mark.parametrize(
"renderer",
[
"--CE",
pytest.param(
"--GL",
marks=pytest.mark.skipif(
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
],
)
@pytest.mark.parametrize(
("klass", "skip_reversing"),
[("BasicSlide", False), ("BasicSlideSkipReversing", True)],
)
def test_skip_reversing(
renderer: str,
slides_file: Path,
manimgl_config: Path,
klass: str,
skip_reversing: bool,
) -> None:
runner = CliRunner()
return Wrapper
with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke(render, [renderer, str(slides_file), klass, "-ql"])
assert results.exit_code == 0, results
local_slides_folder = (Path(tmp_dir) / "slides").resolve(strict=True)
local_config_file = (local_slides_folder / f"{klass}.json").resolve(strict=True)
local_presentation_config = PresentationConfig.from_file(local_config_file)
for slide in local_presentation_config.slides:
if skip_reversing:
assert slide.file == slide.rev_file
else:
assert slide.file != slide.rev_file
def init_slide(cls: SlideType) -> Slide:
if issubclass(cls, CESlide):
return cls()
elif issubclass(cls, GLSlide):
from manimlib.config import parse_cli
_args = parse_cli()
return cls()
raise ValueError(f"Unsupported class {cls}")
parametrize_base_cls = pytest.mark.parametrize(
"base_cls", (CESlide, GLSlide, CEGLSlide), ids=("CE", "GL", "CE(GL)")
)
def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct()
@contextlib.contextmanager
def tmp_cwd() -> Iterator[str]:
cwd = os.getcwd()
tmp_dir = tempfile.mkdtemp()
os.chdir(tmp_dir)
try:
yield tmp_dir
finally:
os.chdir(cwd)
def assert_renders(cls: SlideType) -> None:
with tmp_cwd():
init_slide(cls).render()
class TestSlide:
@assert_constructs
class TestDefaultProperties(Slide):
def construct(self) -> None:
assert self._output_folder == FOLDER_PATH
assert len(self._slides) == 0
assert self._current_slide == 1
assert self._start_animation == 0
assert len(self._canvas) == 0
assert self._wait_time_between_slides == 0.0
def test_default_properties(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._output_folder == FOLDER_PATH
assert len(self._slides) == 0
assert self._current_slide == 1
assert self._start_animation == 0
assert len(self._canvas) == 0
assert self._wait_time_between_slides == 0.0
@pytest.mark.skipif(
version.parse(manim.__version__) < version.parse("0.18"),
reason="Manim change how color are represented in 0.18",
)
@assert_constructs
class TestBackgroundColor(Slide):
def construct(self) -> None:
assert self._background_color == BLACK.to_hex() # DEFAULT
self.camera.background_color = BLUE
assert self._background_color == BLUE.to_hex()
@parametrize_base_cls
def test_frame_height(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
assert self._frame_height > 0 and isinstance(self._frame_height, float)
@assert_renders
class TestMultipleAnimationsInLastSlide(Slide):
"""Check against solution for issue #161."""
@parametrize_base_cls
def test_frame_width(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
assert self._frame_width > 0 and isinstance(self._frame_width, float)
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
@parametrize_base_cls
def test_resolution(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
pw, ph = self._resolution
assert isinstance(pw, int) and pw > 0
assert isinstance(ph, int) and ph > 0
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
@parametrize_base_cls
def test_backround_color(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
assert self._background_color in ["#000000", "#000"] # DEFAULT
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
def test_multiple_animations_in_last_slide(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #161."""
@assert_renders
class TestFileTooLong(Slide):
"""Check against solution for issue #123."""
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
def construct(self) -> None:
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
for _ in range(30):
direction = (random.random() - 0.5) * LEFT + (
random.random() - 0.5
) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
@assert_constructs
class TestLoop(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert not self._base_slide_config.loop
self.next_slide(loop=True)
self.play(text.animate.scale(2))
assert self._base_slide_config.loop
self.next_slide(loop=False)
assert not self._base_slide_config.loop
@assert_constructs
class TestAutoNext(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert not self._base_slide_config.auto_next
self.next_slide(auto_next=True)
self.play(text.animate.scale(2))
assert self._base_slide_config.auto_next
self.next_slide(auto_next=False)
assert not self._base_slide_config.auto_next
@assert_constructs
class TestLoopAndAutoNextFails(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
self.next_slide(loop=True, auto_next=True)
self.play(text.animate.scale(2))
with pytest.raises(ValidationError):
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
@assert_constructs
class TestPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
self.add(text)
def test_file_too_long(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #123."""
assert self._base_slide_config.playback_rate == 1.0
def construct(self) -> None:
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
for _ in range(30):
direction = (random.random() - 0.5) * LEFT + (
random.random() - 0.5
) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
assert self._base_slide_config.playback_rate == 2.0
def test_loop(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
@assert_constructs
class TestReversedPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
self.add(text)
assert not self._base_slide_config.loop
assert self._base_slide_config.reversed_playback_rate == 1.0
self.next_slide(loop=True)
self.play(text.animate.scale(2))
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
assert self._base_slide_config.loop
assert self._base_slide_config.reversed_playback_rate == 2.0
self.next_slide(loop=False)
@assert_constructs
class TestNotes(Slide):
def construct(self) -> None:
text = Text("Some text")
assert not self._base_slide_config.loop
self.add(text)
def test_auto_next(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert self._base_slide_config.notes == ""
self.add(text)
self.next_slide(notes="test")
self.play(text.animate.scale(2))
assert not self._base_slide_config.auto_next
assert self._base_slide_config.notes == "test"
self.next_slide(auto_next=True)
self.play(text.animate.scale(2))
@assert_constructs
class TestWipe(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert self._base_slide_config.auto_next
self.add(text)
self.next_slide(auto_next=False)
assert text in self.mobjects
assert bye not in self.mobjects
assert not self._base_slide_config.auto_next
self.wipe([text], [bye])
def test_loop_and_auto_next_succeeds(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert text not in self.mobjects
assert bye in self.mobjects
self.add(text)
@assert_constructs
class TestZoom(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.next_slide(loop=True, auto_next=True)
self.play(text.animate.scale(2))
self.add(text)
self.next_slide()
assert text in self.mobjects
assert bye not in self.mobjects
def test_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.zoom([text], [bye])
self.add(text)
assert text not in self.mobjects
assert bye in self.mobjects
assert self._base_slide_config.playback_rate == 1.0
@assert_constructs
class TestPlay(Slide):
def construct(self) -> None:
assert self._current_animation == 0
circle = Circle(color=BLUE)
dot = Dot()
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.play(FadeIn(dot))
assert self._current_animation == 2
assert self._base_slide_config.playback_rate == 2.0
@assert_constructs
class TestWaitTimeBetweenSlides(Slide):
def construct(self) -> None:
self._wait_time_between_slides = 1.0
assert self._current_animation == 0
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.next_slide()
assert self._current_animation == 2 # self.wait = +1
def test_reversed_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
@assert_constructs
class TestNextSlide(Slide):
def construct(self) -> None:
assert self._current_slide == 1
self.next_slide()
assert self._current_slide == 1
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
assert self._current_slide == 2
self.next_slide()
assert self._current_slide == 2
self.add(text)
@assert_constructs
class TestCanvas(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert self._base_slide_config.reversed_playback_rate == 1.0
assert len(self.canvas) == 0
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
self.add(text)
assert self._base_slide_config.reversed_playback_rate == 2.0
assert len(self.canvas) == 0
def test_notes(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.add_to_canvas(text=text)
self.add(text)
assert len(self.canvas) == 1
assert self._base_slide_config.notes == ""
self.add(bye)
self.next_slide(notes="test")
self.play(text.animate.scale(2))
assert len(self.canvas) == 1
assert self._base_slide_config.notes == "test"
assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
def test_wipe(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.remove(text)
self.add(text)
assert len(self.canvas) == 1
assert text in self.mobjects
assert bye not in self.mobjects
self.add_to_canvas(bye=bye)
self.wipe([text], [bye])
assert len(self.canvas) == 2
assert text not in self.mobjects
assert bye in self.mobjects
self.remove_from_canvas("text", "bye")
def test_zoom(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert len(self.canvas) == 0
self.add(text)
with pytest.raises(KeyError):
self.remove_from_canvas("text")
assert text in self.mobjects
assert bye not in self.mobjects
self.zoom([text], [bye])
assert text not in self.mobjects
assert bye in self.mobjects
def test_animation_count(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._current_animation == 0
circle = Circle(color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.play(FadeIn(dot))
assert self._current_animation == 2
def test_wait_time_between_slides(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
self._wait_time_between_slides = 1.0
assert self._current_animation == 0
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.next_slide()
assert self._current_animation == 2 # self.wait = +1
def test_next_slide(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._current_slide == 1
self.next_slide()
assert self._current_slide == 1
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
assert self._current_slide == 2
self.next_slide()
assert self._current_slide == 2
def test_next_slide_skip_animations(self) -> None:
class Foo(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.next_slide(skip_animations=True)
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert self._base_slide_config.skip_animations
self.next_slide()
assert not self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
class Bar(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.next_slide(skip_animations=False)
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert not self._base_slide_config.skip_animations
self.next_slide()
assert not self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
class Baz(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.start_skip_animations()
self.next_slide()
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert self._base_slide_config.skip_animations
self.next_slide()
assert self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
self.stop_skip_animations()
with tmp_cwd() as tmp_dir:
init_slide(Foo).render()
init_slide(Bar).render()
init_slide(Baz).render()
slides_folder = Path(tmp_dir) / "slides"
assert slides_folder.exists()
slide_file = slides_folder / "Foo.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 2
slide_file = slides_folder / "Bar.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 3
slide_file = slides_folder / "Baz.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 1
def test_canvas(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert len(self.canvas) == 0
self.add(text)
assert len(self.canvas) == 0
self.add_to_canvas(text=text)
assert len(self.canvas) == 1
self.add(bye)
assert len(self.canvas) == 1
assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
self.remove(text)
assert len(self.canvas) == 1
self.add_to_canvas(bye=bye)
assert len(self.canvas) == 2
self.remove_from_canvas("text", "bye")
assert len(self.canvas) == 0
with pytest.raises(KeyError):
self.remove_from_canvas("text")

View File

@ -69,7 +69,7 @@ class TestWizard:
monkeypatch.setattr(QMessageBox, "exec", exec_patched)
for i, (key, _) in enumerate(wizard.config.keys.dict().items()):
for i, (key, _) in enumerate(wizard.config.keys.model_dump().items()):
open_dialog(i, getattr(wizard.config.keys, key))
wizard.button_box.accepted.emit()
@ -89,7 +89,7 @@ def test_init() -> None:
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
def test_init_custom_path() -> None:
@ -106,7 +106,7 @@ def test_init_custom_path() -> None:
assert results.exit_code == 0
assert not CONFIG_PATH.exists()
assert custom_path.exists()
assert Config().dict() == Config.from_file(custom_path).dict()
assert Config().model_dump() == Config.from_file(custom_path).model_dump()
def test_init_path_exists() -> None:
@ -120,7 +120,7 @@ def test_init_path_exists() -> None:
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
results = runner.invoke(init, input="o")
@ -156,7 +156,7 @@ def test_wizard(monkeypatch: MonkeyPatch) -> None:
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
def test_wizard_closed_without_saving(monkeypatch: MonkeyPatch) -> None:

4291
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff