Compare commits

..

167 Commits

Author SHA1 Message Date
3e0268a431 chore(deps): bump version from 5.5.0 to 5.5.1 2025-03-28 15:26:55 +01:00
6e14dc9051 feat(html): pause HTML slides with SPACE key (#539)
* feat(html): pause HTML slides with `SPACE` key

Pressing <kbd>SPACE</kbd> key now pauses the slides, instead of skipping it.

* 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-03-28 15:25:53 +01:00
4a400398b8 chore(deps): pre-commit autoupdate (#537)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-28 15:24:58 +01:00
d641d2d82c feat(html): always include RevealJS' notes plugin (#538)
This allows to open the speaker view no matter if we had notes included.
2025-03-28 15:23:53 +01:00
d3396d3a01 chore(deps): bump version from 5.4.2 to 5.5.0 2025-03-24 15:05:53 +01:00
7a922db6f1 chore(dev): lock 2025-03-24 15:05:46 +01:00
9b9593985d chore(docs): add missing changelog entry from #536 2025-03-24 15:04:12 +01:00
c915af19e8 chore(html): bump RevealJS version to 5.2 (#536)
Bump version as it includes a nice fix for speaker view: background videos are now played.

See: https://github.com/hakimel/reveal.js/pull/1037#issuecomment-1825352783.
2025-03-24 15:01:27 +01:00
a8897552d8 chore(docs): remove link to survey 2025-03-24 15:00:34 +01:00
b02dbbcc8c chore(deps): pre-commit autoupdate (#532)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.6 → v0.11.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.6...v0.11.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-24 14:28:32 +01:00
c6ba210797 chore(docs): specify canonical URL 2025-02-25 11:42:47 +01:00
b1212a49d3 chore(docs): all-versions Zenodo DOI 2025-02-24 14:05:29 +01:00
f7ce5fc115 chore(deps): pre-commit autoupdate (#530)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.4 → v0.9.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.4...v0.9.6)
- [github.com/pre-commit/mirrors-mypy: v1.14.1 → v1.15.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.1...v1.15.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-10 23:11:37 +01:00
5f9bbf2a79 chore(deps): pre-commit autoupdate (#528)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.3 → v0.9.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.3...v0.9.4)
- [github.com/codespell-project/codespell: v2.4.0 → v2.4.1](https://github.com/codespell-project/codespell/compare/v2.4.0...v2.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-04 15:27:42 +01:00
ccbe9d558c feat(lib): allow to insert external videos as slides (#526)
* feat(lib): allow to insert external videos as slides

See https://github.com/jeertmans/manim-slides/discussions/520

* chore(lib): lint and changelog entry

* chore: fix PR #

fix

* fix: docs
2025-01-29 19:17:45 +01:00
a2bd1ffb67 chore(convert): improve HTML Jinja2 template (#443)
* chore(convert): improve HTML Jinja2 template

As suggested by @yunusey in #442

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

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

* chore(deps): fix importlib

* fix: remove redundant if

* fix(docs): rename PeculiarProgrammer to taibeled

* chore: update template and doc. example

* chore(docs): add changelog entry

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-29 11:01:37 +01:00
e911ec3096 feat(lib): smarter files reversing (#439)
* feat(lib): smarter files reversing

Implement a smarter generation of reversed files by splitting the video into smaller segments.

Closes #434

* chore(lib): change default length

* chore: use suffix

* chore(docs): update

* chore(docs): fix docs and add changelog entry

* chore(tests): coverage

* chore(docs): typo
2025-01-28 23:05:59 +00:00
daf547414c chore(deps): pre-commit autoupdate (#525)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.2 → v0.9.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.2...v0.9.3)
- [github.com/codespell-project/codespell: v2.3.0 → v2.4.0](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-28 16:46:13 +01:00
528952dbc3 chore(deps): bump version from 5.4.1 to 5.4.2 2025-01-22 21:43:00 +01:00
dbced6e62e fix(lib): pass skip argument to ManimCE (#524)
* fix(lib): pass skip argument to ManimCE

* fix(lint): make ruff happy
2025-01-22 21:41:22 +01:00
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
14df6eb55a chore(docs): update changelog 2024-04-18 22:14:35 +02:00
ef014ebac6 chore(version): bump 5.1.4 to 5.1.5 2024-04-18 22:13:11 +02:00
d5d1513d94 chore(dev): move to Rye instead of PDM (#420)
* test: re-add opencv-python

* chore(dev): move to Rye instead of PDM

* try fix

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

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

* fix: build backend

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

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

* fix: string quotes?

* small fixes

* upgrade typing

* fix(ci): rye install on Windows

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

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

* fix(ci): typos

* fix

* fix(ci): actually use right python version

* fix(deps): manimgl

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

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

* fix docs

* another fix

* cleanup

* make sure to use trusted publisher

* chore(docs): remove PDM

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-18 22:12:45 +02:00
bd04dae2bf fix(cli): broken --show-config command (#419)
Fix and improve the command.
2024-04-17 20:33:44 +02:00
d5c20d1791 chore(docs): update faq page 2024-04-17 20:07:15 +02:00
527ce2767e chore(docs): add faq page (#418)
* chore(docs): add faq page

Closes #405

* chore(docs): add faq page

Closes #405

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

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

* update changelog

* fix

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-17 18:10:46 +02:00
04bca458f1 chore(ci): checking links and spell checking (#417)
* chore(ci): checking links and spell checking

* chore(ci): move markdown-link-check to GitHub ci

Because pre-commit.ci does not have access to the internet...

* fix(lib): revert `reverse-...` utils because of warnings

* chore(ci): checking links and spell checking

* chore(ci): move markdown-link-check to GitHub ci

Because pre-commit.ci does not have access to the internet...

* fix(docs): myst-parser xref cannot end with .html

* fix(docs): oops
2024-04-17 17:59:40 +02:00
4e7abe8706 fix(lib): revert reverse-... utils because of warnings 2024-04-17 17:02:43 +02:00
49e2c31d9a chore(version): bump 5.1.3 to 5.1.4 2024-04-16 17:35:42 +02:00
5920a843f5 Merge remote-tracking branch 'origin/main' into main 2024-04-16 17:35:26 +02:00
59dd365291 chore(deps): remove OpenCV python (#415)
Closes #413
2024-04-16 17:35:23 +02:00
3e2e64b09f chore(docs): update changelog 2024-04-16 17:25:03 +02:00
8a3bf87db8 chore(lib): filter out video files without video stream (#416)
* chore(lib): filter out video files without video stream

This is a ( hopefully temporary) fix to #390.

Closes #390

* fix: Windows issue
2024-04-16 17:17:42 +02:00
498e9af2bf fix(lib): correctly retrieve background color (#414)
Closes #409
2024-04-16 14:33:27 +02:00
24ee23af11 chore(deps): update RevealJS version to 5.1.0 (#412)
Closes #384
2024-04-16 13:53:08 +02:00
a775c4989b [pre-commit.ci] pre-commit autoupdate (#411)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-16 13:23:08 +02:00
04f6ee7f9b [pre-commit.ci] pre-commit autoupdate (#403)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-11 09:52:12 +02:00
bbc539b461 chore(docs): add nixpkgs installation documentation (#404) 2024-04-11 09:30:40 +02:00
2d9d263c9c chore(docs): add favicon (#399)
* chore(docs): add favicon

* fix(docs): typo
2024-04-04 14:40:36 +02:00
cfa9c082ab [pre-commit.ci] pre-commit autoupdate (#374)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.12.0 → v2.13.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.12.0...v2.13.0)
- [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.3.5)
- [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-04 11:14:40 +02:00
67533c460e feat(cli): added separate option for info window screen (#389)
Closes #388.
2024-04-04 11:14:25 +02:00
a85f1c4036 fix(docs): README url 2024-03-08 11:59:08 +01:00
b3fe6f17b9 chore(cite): update citation file 2024-03-07 10:26:04 +01:00
e7182a445d feat(present): add audio output (#382)
* feat(present): add audio output

This PR adds the necessary audio output to the presenter mode, so it can play audio contained in video files, see #375.

However, this does not fix the issue that slides do not contain audio by default, but should be solved by Manim, see #375 for updates on that topic.

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

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

* fix: PR number

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-05 11:08:30 +01:00
1dbd2fdde5 chore(version): bump 5.1.2 to 5.1.3 2024-02-16 13:58:43 +01:00
07fd2bdcf1 chore(lib): warn about non-preferred-qt-version (#373)
Closes #371
2024-02-16 13:57:54 +01:00
d586dab102 [pre-commit.ci] pre-commit autoupdate (#372)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1)

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

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

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-06 08:48:42 +01:00
92c569950c Fix link to documentation in README. (#368)
* Fix link to documentation in README.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-02 09:54:47 +01:00
648d7ff921 chore(version): bump 5.1.1 to 5.1.2 2024-02-01 13:02:40 +01:00
b47068ede5 fix(rtd): remove flyout in iframes (#367)
* fix(rtd): remove flyout in iframes

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

Because dirhtml seems to have issues with iframes

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

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

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

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

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

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

---------

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

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

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

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

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

* chore(deps): some fixes but wip

* chore(docs): update

* chore(deps): support PyQt6

* chore(deps): make Qt backend optional

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

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

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

* chore(deps): some fixes but wip

* chore(docs): update

* chore(deps): support PyQt6

* fix(deps): ci and docs

* fix(lib): missing package

* chore(ci): does it work?

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

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

* chore(test): skip failing

* chore(docs): update

* chore(docs): update

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

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

* fix(docs): typo

* fix(test): quit instead of shutdown

---------

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

---
updated-dependencies:
- dependency-name: ssciwr/setup-mesa-dist-win
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-26 12:00:39 +01:00
39ff80ed1e chore(deps): bump pdm-project/setup-pdm from 3 to 4 (#360)
Bumps [pdm-project/setup-pdm](https://github.com/pdm-project/setup-pdm) from 3 to 4.
- [Release notes](https://github.com/pdm-project/setup-pdm/releases)
- [Commits](https://github.com/pdm-project/setup-pdm/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-26 12:00:25 +01:00
b84a22d0f9 [pre-commit.ci] pre-commit autoupdate (#358)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.13 → v0.1.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.13...v0.1.14)

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

Shows an example of custom template.

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

Closes #356

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

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

* chore(docs): document changes and fix

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-23 11:07:36 +01:00
02a8173ed1 fix(lib): reversed video quality 2024-01-19 13:27:40 +01:00
07dff32be3 chore(ci): rename 2024-01-18 23:36:02 +01:00
84 changed files with 8679 additions and 4826 deletions

View File

@ -1,16 +0,0 @@
[bumpversion]
current_version = 5.1.0-rc3
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

27
.github/scripts/check_github_issues.py vendored Normal file
View File

@ -0,0 +1,27 @@
"""Check that GitHub issues (and PR) links match the number in Markdown link."""
import glob
import re
import sys
if __name__ == "__main__":
p = re.compile(
r"\[#(?P<number1>[0-9]+)\]"
r"\(https://github\.com/"
r"(?:[a-zA-Z0-9_-]+)/(?:[a-zA-Z0-9_-]+)/"
r"(?:(?:issues)|(?:pull))/(?P<number2>[0-9]+)\)"
)
ret_code = 0
for glob_pattern in sys.argv[1:]:
for file in glob.glob(glob_pattern, recursive=True):
with open(file) as f:
for i, line in enumerate(f):
for m in p.finditer(line):
if m.group("number1") != m.group("number2"):
start, end = m.span()
print(f"{file}:{i}: ", line[start:end], file=sys.stderr) # noqa: T201
ret_code = 1
sys.exit(ret_code)

View File

@ -1,97 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: [main]
pull_request:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: pages
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
permissions: write-all
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v3
with:
python-version: '3.10'
cache: true
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install Linux Dependencies
run: |
sudo apt-get update
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Setup Pandoc
uses: nikeee/setup-pandoc@v1
- name: Install local Python package
run: pdm install -dGdocs -dGgithub-action
- name: Install IPython kernel
run: pdm run ipython kernel install --name "manim-slides" --user
- name: Restore cached media
id: cache-media-restore
uses: actions/cache/restore@v4
with:
path: docs/media
key: ${{ runner.os }}-docs-media
- name: Clear cache
run: |
gh extension install actions/gh-actions-cache
gh actions-cache delete ${{ steps.cache-media-restore.outputs.cache-primary-key }} --confirm || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save media to cache
id: cache-media-save
uses: actions/cache/save@v4
with:
path: docs/media
key: ${{ steps.cache-media-restore.outputs.cache-primary-key }}
- name: Build docs
run: cd docs && pdm run make html
- name: Upload artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
# Upload docs/build/html dir
path: docs/build/html/
- name: Show docs/build/html/_static/ dir content (video only)
run: tree -L 3 docs/build/html/_static/ -P '*.mp4'
- name: Deploy to GitHub Pages
id: deployment
if: github.event_name != 'pull_request'
uses: actions/deploy-pages@v4

View File

@ -8,7 +8,7 @@ on:
jobs:
publish-python:
name: Build and release
name: Publish Python package
runs-on: ubuntu-latest
environment: release
permissions:
@ -18,23 +18,24 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v3
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
python-version: '3.10'
cache: true
enable-cache: true
- name: Build package
run: uv build
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
run: pdm publish
uses: pypa/gh-action-pypi-publish@release/v1
publish-docker:
name: Publish Docker image
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}`
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v4
@ -61,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: |
jeertmans/manim-slides:latest
jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
ghcr.io/jeertmans/manim-slides:latest
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}

View File

@ -7,56 +7,84 @@ 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
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.8', '3.9', '3.10', '3.11']
pyversion: ['3.9', '3.10', '3.11', '3.12']
runs-on: ${{ matrix.os }}
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: error
PYTHONFAULTHANDLER: 1
DISPLAY: :99
GITHUB_WORKFLOWS: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v3
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.pyversion }}
cache: true
enable-cache: true
# Path related stuff
- name: Append to Path on MacOS
if: matrix.os == 'macos-latest'
run: |
echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
- name: Append to Path on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
- name: Append to Path on Windows
if: matrix.os == 'windows-latest'
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
# OS depedencies
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
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
@ -65,24 +93,27 @@ jobs:
- name: Install Mesa
if: matrix.os == 'windows-latest'
uses: ssciwr/setup-mesa-dist-win@v1
- name: Install Manim Slides
run: |
pdm install -dGgithub-action -dGtest
uses: ssciwr/setup-mesa-dist-win@v2
- name: Run pytest
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
run: pdm run pytest
- name: Run pytest and coverage
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
run: pdm 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@v3
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
fail_ci_if_error: true
markdown-link-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: yes
use-verbose-mode: yes
config-file: .mdlc.json

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

8
.mdlc.json Normal file
View File

@ -0,0 +1,8 @@
{
"replacementPatterns": [
{
"pattern": "^/(?<path>.*)$",
"replacement": "https://eertmans.be/manim-slides/latest/$<path>.html"
}
]
}

View File

@ -1,31 +1,47 @@
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.5.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.12.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.1.13
rev: v0.11.2
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:
- tomli
- repo: local
hooks:
- id: github-issues
name: GitHub issues link check
description: Check issues (and PR) links are matching number.
entry: python .github/scripts/check_github_issues.py
language: system
types: [markdown]

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.11

18
.readthedocs.yaml Normal file
View File

@ -0,0 +1,18 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: '3.11'
apt_packages:
- libpango1.0-dev
- ffmpeg
sphinx:
builder: html
configuration: docs/source/conf.py
fail_on_warning: true
python:
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -7,8 +7,357 @@ 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.5.1...HEAD)
(v5.5.1)=
## [v5.5.1](https://github.com/jeertmans/manim-slides/compare/v5.5.0...v5.5.1)
(v5.5.1-changed)=
### Changed
- HTML template now always includes the *notes* plugin so that the speaker
view is always available. Previously, it was only included if the slides
had notes.
[#538](https://github.com/jeertmans/manim-slides/pull/538)
- Pressing <kbd>SPACE</kbd> key now pauses the slides, instead of skipping it.
Previously, it was not possible to pause HTML slides, which can be very annoying
when trying to explain something.
[#539](https://github.com/jeertmans/manim-slides/pull/539)
(v5.5.0)=
## [v5.5.0](https://github.com/jeertmans/manim-slides/compare/v5.4.2...v5.5.0)
(v5.5.0-added)=
### Added
- Added `max_duration_before_split_reverse` and `num_processes` class variables.
[#439](https://github.com/jeertmans/manim-slides/pull/439)
- Added `src = ...` filepath argument to allow inserting external
videos as slides.
[#526](https://github.com/jeertmans/manim-slides/pull/526)
(v5.5.0-changed)=
### Changed
- Automatically split large video animations into smaller chunks
for lightweight (and potentially faster) reversed animations generation.
[#439](https://github.com/jeertmans/manim-slides/pull/439)
(v5.5.0-chore)=
### Chore
- Trimmed whitespaces in HTML template.
[#443](https://github.com/jeertmans/manim-slides/pull/443)
- Bumped RevealJS' version to 5.2 to allow video playing in speaker view.
[#536](https://github.com/jeertmans/manim-slides/pull/536)
(v5.4.2)=
## [v5.4.2](https://github.com/jeertmans/manim-slides/compare/v5.4.1...v5.4.2)
(v5.4.2-fixed)=
### Fixed
- Fixed `start_skip_animations` to actually pass argument to ManimCE,
otherwise video animations were still rendered, just excluded from
the final output.
[#524](https://github.com/jeertmans/manim-slides/pull/524)
(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.
[@taibeled](https://github.com/taibeled)
[#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 [@taibeled](https://github.com/taibeled).
[#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 [@taibeled](https://github.com/taibeled)!
[#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-chore)=
### Chore
- Added CI for broken HTML links and fixed, plus spell checking.
[#417](https://github.com/jeertmans/manim-slides/pull/417)
- Create FAQ page and clear FAQ from README.md.
[#418](https://github.com/jeertmans/manim-slides/pull/418)
- Used Rye instead of PDM for faster development.
[#420](https://github.com/jeertmans/manim-slides/pull/420)
(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)
(v5.1.4-added)=
### Added
- Added audio output to `manim-slides present`.
[#382](https://github.com/jeertmans/manim-slides/pull/382)
(v5.1.4-changed)=
### Changed
- Added `--info-window-screen` option and change `--screen-number`
to not move the info window.
[#389](https://github.com/jeertmans/manim-slides/pull/389)
(v5.1.4-chore)=
### Chore
- Created a favicon for the website/documentation.
[#399](https://github.com/jeertmans/manim-slides/pull/399)
- Documented the Nixpkg installation.
[#404](https://github.com/jeertmans/manim-slides/pull/404 )
- Updated the default RevealJS version to 5.1.0.
[#412](https://github.com/jeertmans/manim-slides/pull/412)
- Removed the `opencv-python` dependency.
[#415](https://github.com/jeertmans/manim-slides/pull/415)
(v5.1.4-fixed)=
### Fixed
- Fixed the retrieval of `background_color` with ManimCE.
[#414](https://github.com/jeertmans/manim-slides/pull/414)
- Fixed #390 issue caused by empty media created by ManimCE.
[#416](https://github.com/jeertmans/manim-slides/pull/416)
(v5.1.3)=
## [v5.1.3](https://github.com/jeertmans/manim-slides/compare/v5.1.2...v5.1.3)
(v5.1.3-chore)=
### Chore
- Fix link in documentation.
[#368](https://github.com/jeertmans/manim-slides/pull/368)
- Warn users if not using recommended Qt bindings.
[#373](https://github.com/jeertmans/manim-slides/pull/373)
(v5.1.2)=
## [v5.1.2](https://github.com/jeertmans/manim-slides/compare/v5.1.1...v5.1.2)
(v5.1.2-chore)=
### Chore
- Fix ReadTheDocs version flyout in iframes.
[#367](https://github.com/jeertmans/manim-slides/pull/367)
(v5.1.1)=
## [v5.1.1](https://github.com/jeertmans/manim-slides/compare/v5.1.0...v5.1.1)
(v5.1.1-chore)=
### Chore
- Move documentation to ReadTheDocs for better versioning.
[#365](https://github.com/jeertmans/manim-slides/pull/365)
(v5.1)=
## [v5.1 (Unreleased)](https://github.com/jeertmans/manim-slides/compare/v5.0.0...HEAD)
## [v5.1](https://github.com/jeertmans/manim-slides/compare/v5.0.0...v5.1.0)
(v5.1-added)=
### Added
@ -32,6 +381,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#331](https://github.com/jeertmans/manim-slides/pull/331)
- Created a Docker image, published on GitHub.
[#355](https://github.com/jeertmans/manim-slides/pull/355)
- Added `:template:` and `:config_options` options to
the Sphinx directive.
[#357](https://github.com/jeertmans/manim-slides/pull/357)
(v5.1-modified)=
### Modified
@ -44,11 +396,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.1-chore)=
### Chore
- Removed subrocess calls to FFMPEG with direct `libav` bindings using
- Removed subrocess calls to FFmpeg with direct `libav` bindings using
the `av` Python module. This should enhance rendering speed and security.
[#335](https://github.com/jeertmans/manim-slides/pull/335)
- Changed build backend to PDM and reflected on docs.
[#354](https://github.com/jeertmans/manim-slides/pull/354)
- Dropped Python 3.8 support.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Made Qt backend optional and support PyQt6 too.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Documentated how to create and use a custom HTML template.
[#357](https://github.com/jeertmans/manim-slides/pull/357)
## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0)
@ -112,7 +470,7 @@ In an effort to better document changes, this CHANGELOG document is now created.
but the new player should be much easier to maintain and more performant,
than its predecessor.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Changed the slide config format to exclude unecessary information.
- Changed the slide config format to exclude unnecessary information.
`StypeType` is removed in favor to one boolean `loop` field. This is
a **breaking change** and one should re-render the slides to apply changes.
[#243](https://github.com/jeertmans/manim-slides/pull/243)

View File

@ -3,25 +3,23 @@
cff-version: 1.2.0
title: Manim Slides
message: A Python package for presenting Manim content anywhere
message: >-
If you use this software, please cite it using our article
in the Journal of Open Source Education.
type: software
authors:
- name: Jérome Eertmans
orcid: 'https://orcid.org/0000-0002-5579-5360'
website: 'https://eertmans.be'
identifiers:
- type: doi
value: 10.21105/jose.00206
description: The paper presenting the software.
doi: 10.5281/zenodo.7971360
repository-code: 'https://github.com/jeertmans/manim-slides'
url: 'https://eertmans.be/manim-slides'
abstract: >-
Manim Slides is a Python package that makes presenting
Manim animations straightforward. With minimal changes
required to pre-existing code, one can slide through
animations in a PowerPoint-like manner, or share its
slides online using ReavealJS power.
slides online using ReavealJS' power.
keywords:
- Education
- Math Animations
@ -29,4 +27,19 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.1.0-rc3
version: v5.5.1
preferred-citation:
publisher:
name: The Open Journal
type: article
authors:
- name: Jérome Eertmans
orcid: 'https://orcid.org/0000-0002-5579-5360'
doi: 10.21105/jose.00206
journal: Journal of Open Source Education
month: 8
year: 2023
title: 'Manim Slides: A Python package for presenting Manim content anywhere'
volume: 6
number: 66
pages: 206

View File

@ -4,6 +4,8 @@
<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]
@ -11,6 +13,7 @@
[![DOI][doi-badge]][doi-url]
[![JOSE Paper][jose-badge]][jose-url]
[![codecov][codecov-badge]][codecov-url]
[![Binder][binder-badge]][binder-url]
# Manim Slides
@ -27,8 +30,8 @@ Manim Slides will *automatically* detect the one you are using!
- [Installation](#installation)
- [Usage](#usage)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
- [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)
@ -38,7 +41,7 @@ Manim Slides will *automatically* detect the one you are using!
Manim Slides requires either Manim or ManimGL to be installed, along
with their dependencies. Please checkout the
[documentation](https://eertmans.be/manim-slides/installation.html)
[documentation](https://eertmans.be/manim-slides/latest/installation.html)
for detailed install instructions.
## Usage
@ -48,7 +51,7 @@ for detailed install instructions.
Using Manim Slides is a two-step process:
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead
of `Scene` (resp. `ThreeDScene`), and add calls to `self.next_slide()`
everytime you want to create a new slide.
every time you want to create a new slide.
2. Run `manim-slides` on rendered animations and display them like a
*PowerPoint* presentation.
@ -56,7 +59,7 @@ The documentation is available [online](https://eertmans.be/manim-slides/).
### Basic Example
Call `self.next_slide()` everytime you want to create a pause between
Call `self.next_slide()` every time you want to create a pause between
animations, and `self.next_slide(loop=True)` if you want the next slide to loop
over animations until the user presses continue:
@ -88,11 +91,15 @@ manim-slides render example.py BasicExample
# or use ManimGL
manim-slides render --GL example.py BasicExample
```
<!-- end usage -->
> [!NOTE]
> Using `manim-slides render` makes sure the use the `manim`
> (or `manimlib`) library that was installed in the Python same environment.
> Put simply, this is a wrapper of `manim render [ARGS]...` (or `manimgl [ARGS]...`).
> Using `manim-slides render` makes sure to use the `manim`
> (or `manimlib`) library that was installed in the same Python environment.
> Put simply, this is a wrapper around
> `manim render [ARGS]...` (or `manimgl [ARGS]...`).
<!-- start more-usage -->
To start the presentation using `Scene1`, `Scene2` and so on, run:
@ -106,14 +113,14 @@ In our example:
manim-slides BasicExample
```
<!-- end usage -->
<!-- end more-usage -->
<p align="center">
<img alt="Example GIF" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif">
</p>
For detailed usage documentation, run `manim-slides --help`, or go to the
[documentation](https://eertmans.be/manim-slides/reference/cli.html).
[documentation](https://eertmans.be/manim-slides/latest/reference/cli.html).
## Interactive Tutorial
@ -145,27 +152,28 @@ 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
## F.A.Q
## Citing
### How to increase quality on Windows
If you use this project, please cite it using the following reference:
On Windows platform, one may encounter a lower image resolution than expected.
Usually, this is observed because Windows rescales every application to
fit the screen.
As found by [@arashash](https://github.com/arashash),
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
the problem can be addressed by changing the scaling factor to 100%:
```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}
}
```
<p align="center">
<img alt="Windows Fix Scaling" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png">
</p>
in *Settings*->*Display*.
or by linking this GitHub repository at the end of the presentation.
## Contributing
Contributions are more than welcome! Please read through
[our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
[our contributing section](https://eertmans.be/manim-slides/latest/contributing/index.html).
### Reporting an Issue
@ -197,12 +205,10 @@ be able to help you!
Sometimes, you may have a question about Manim Slides,
not necessarily an issue.
There are two ways you can reach us for questions:
- via the `Question/Help/Support` topic when
[choosing an issue template](https://github.com/jeertmans/manim-slides/issues/new/choose);
- or via
[GitHub discussions](https://github.com/jeertmans/manim-slides/discussions).
First, make sure to read the
[F.A.Q](https://eertmans.be/manim-slides/latest/faq.html) to see if
your question has already been answered. If not, please follow the
recommendation (from that page) to reach us for questions.
<!-- end seeking-for-help -->
@ -220,12 +226,13 @@ you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
[pypi-version-url]: https://pypi.org/project/manim-slides/
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/manim-slides
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
[documentation-badge]: https://img.shields.io/website?down_color=lightgrey&down_message=offline&label=documentation&up_color=green&up_message=online&url=https%3A%2F%2Feertmans.be%2Fmanim-slides%2F
[documentation-url]: https://eertmans.be/manim-slides/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
[documentation-url]: https://manim-slides.readthedocs.io/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7971360.svg
[doi-url]: https://doi.org/10.5281/zenodo.7971360
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
[jose-url]: https://doi.org/10.21105/jose.00206
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
[codecov-url]: https://codecov.io/gh/jeertmans/manim-slides
[binder-badge]: https://mybinder.org/badge_logo.svg
[binder-url]: https://mybinder.org/v2/gh/jeertmans/manim-slides-binder/HEAD?filepath=getting_started.ipynb

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

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
from datetime import date
@ -30,9 +31,11 @@ extensions = [
# Additional
"nbsphinx",
"myst_parser",
"sphinxcontrib.programoutput",
"sphinxext.opengraph",
"sphinx_click",
"sphinx_copybutton",
"sphinx_design",
# Custom
"manim_slides.docs.manim_slides_directive",
]
@ -53,8 +56,10 @@ add_module_names = False
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
html_theme = "furo"
html_static_path = ["_static"]
html_favicon = "_static/favicon.png"
html_theme_options = {
"light_logo": "logo_light_transparent.png",

View File

@ -18,10 +18,10 @@ workflow
internals
```
[Workflow](./workflow)
[Workflow](/contributing/workflow)
: how to work on this project. Start here if you're a new contributor.
[Internals](./internals)
[Internals](/contributing/internals)
: how Manim Slides is built and how the various parts of it work.
## Reporting an Issue

View File

@ -4,7 +4,7 @@ This document is there to help you recreate a working environment for Manim Slid
## Dependencies
```{include} ../quickstart.md
```{include} ../installation.md
:start-after: <!-- start deps -->
:end-before: <!-- end deps -->
```
@ -18,65 +18,36 @@ Useful links:
* [GitHub's Hello World](https://docs.github.com/en/get-started/quickstart/hello-world).
* [GitHub Pull Request in 100 Seconds](https://www.youtube.com/watch?v=8lGpZkjnkt4&ab_channel=Fireship).
Once you feel comfortable with git and GitHub, [fork](https://github.com/jeertmans/manim-slides/fork) the repository, and clone it locally.
Once you feel comfortable with git and GitHub,
[fork](https://github.com/jeertmans/manim-slides/fork)
the repository, and clone it locally.
As for every Python project, using virtual environment is recommended to avoid
conflicts between modules.
For this project, we use [PDM](https://pdm-project.org/) 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 PDM, installation becomes straightforward:
With uv, installation becomes straightforward:
```bash
pdm install
```
This, however, only installs the minimal set of dependencies to run the package.
If you would like to install Manim or ManimGL,
as documented in the [quickstart](../quickstart),
you can use the `-G|--group` option:
```bash
pdm install -Gmanim # For Manim
# or
pdm install -Gmanimgl # For ManimGL
```
Additionnally, Manim Slides comes with groups of dependencies for development purposes:
```bash
pdm install -dGdev # For linters and formatters
# or
pdm install --dGdocs # To build the documentation locally
# or
pdm install --dGtests # To run tests
uv sync --all-extras
```
:::{note}
You can combine any number of groups or extras when installing the package locally.
You can also install everything with `pdm install -G:all`.
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 `pdm run` to any command, e.g.:
Instead, you either need to prepend `uv run` to any command, e.g.:
```bash
pdm run manim-slides wizard
```
or [enter a new shell](https://pdm-project.org/latest/usage/venv/#activate-a-virtualenv)
that uses this new Python environment:
```bash
eval $(pdm venv activate) # Click on the link above to see shell-specific command
manim-slides wizard
uv run manim-slides wizard
```
## Testing your code
@ -85,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
pdm 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
@ -95,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
pdm run pytest
uv run pytest
```
## Building the documentation
@ -107,7 +78,7 @@ To generate the documentation, run the following:
```bash
cd docs
pdm run make html
uv run make html
```
Then, the output index file is located at `docs/build/html/index.html` and

124
docs/source/faq.md Normal file
View File

@ -0,0 +1,124 @@
# Frequently Asked Questions
This page summarizes a few of the most frequently asked questions
when using Manim Slides.
They are organized by topic.
If your question is not here, please first look through the
[open **and closed** issues on GitHub](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue)
or within the [discussions](https://github.com/jeertmans/manim-slides/discussions).
If you still cannot find help after that, do not hesitate to create
your own issue or discussion on GitHub!
## Installing
Everything related to installing Manim-Slides.
Please do not forget the carefully read through
the [installation](/installation) page!
## Rendering
Questions related to `manim-slides render [SCENES]...`,
### I cannot render with ManimGL
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 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
Questions related to `manim-slides present [SCENES]...`,
or `manim-slides [SCENES]...` for short.
### Can I have interactive slides
No. Slides are pre-rendered static videos files
and cannot be modified on the fly.
If you need new to have some kind of interactive, look
at the preview feature coupled with the OpenGL renderer
with ManimCE or ManimGL.
### Slides go black when video finishes
This is an issue with Qt,
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.
Usually, this is observed because Windows rescales every application to
fit the screen.
As found by [@arashash](https://github.com/arashash),
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
the problem can be addressed by changing the scaling factor to 100%:
<p align="center">
<img
alt="Windows Fix Scaling"
src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png"
>
</p>
in *Settings*->*Display*.
## Converting to any format
Questions that apply to all output formats when using
`manim-slides convert [SCENES]...`.
### What are all possible configuration options
Configuration options can be specified with the syntax
`-c<option_name>=<option_value>`.
To list all accepted options, use `manim-slides convert --to=FORMAT --show-config`,
where `FORMAT` is one of the supported formats.
This will also show the default value for each option.
### How to retrieve the current template
If you want to create your own template, the best is to start from the default one.
You can either download it from the
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/templates)
or use the `manim-slides convert --to=FORMAT --show-template` command,
where `FORMAT` is one of the supported formats.
## Converting to HTML
Questions related to `manim-slides convert [SCENES]... output.html`.
### I moved my `.html` file and it stopped working
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,
you need to move the HTML **and its assets** altogether.
## Converting to PPTX
Questions related to `manim-slides convert [SCENES]... output.pptx`.
### My media stop playing after a few slides
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

@ -23,7 +23,7 @@ og:description: Manim Slides makes creating slides with Manim super easy!
Manim Slides makes creating slides with Manim super easy!
In a [very few steps](./quickstart),
In a [very few steps](/quickstart),
you can create slides and present them either using the GUI, or your browser.
Slide through the demo below to get a quick glimpse on what you can do with
@ -43,6 +43,8 @@ installation
reference/index
features_table
manim_or_manimgl
gallery
faq
```
```{toctree}

View File

@ -12,12 +12,14 @@ The benefit of using pipx is that it will automatically create a new virtual
environment for every package you install.
:::{note}
Everytime you read `pipx install`, you can use `pip install` instead,
Every time you read `pipx install`, you can use `pip install` instead,
if you are working in a virtual environment or else.
:::
## Dependencies
<!-- start deps -->
Manim Slides requires either Manim or ManimGL to be installed, along
with their dependencies.
Having both packages installed is fine too.
@ -27,19 +29,15 @@ 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).
:::
<!-- end deps -->
## Pip Install
The recommended way to install the latest release is to use pip:
The recommended way to install the latest release
with all features is to use pipx:
```bash
pipx install -U manim-slides
pipx install -U "manim-slides[pyside6-full]"
```
:::{tip}
@ -48,6 +46,28 @@ like to upgrade to the latest version available,
if Manim Slides is already installed.
:::
:::{note}
The quotes `"` are added because not all shell support unquoted
brackets (e.g., zsh) or commas (e.g., Windows).
:::
You can check that Manim Slides was correctly installed with:
```bash
manim-slides --version
```
## Custom install
If you want more control on what dependencies are installed,
you can always install the bare minimal dependencies with:
```bash
pipx install -U manim-slides
```
And install additional dependencies later.
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
@ -56,11 +76,8 @@ pipx install -U "manim-slides[manim]" # For Manim
pipx install -U "manim-slides[manimgl]" # For ManimGL
```
You can check that Manim Slides was correctly installed with:
```bash
manim-slides --version
```
For optional dependencies documentation, see
[next section](#optional-dependencies).
:::{warning}
If you are installing with pipx, this is mandatory to at least include
@ -70,20 +87,27 @@ either `manim` or `manimgl`.
[^1]: You still need to have Manim or ManimGL platform-specific dependencies
installed on your computer.
## Optional Dependencies
## Optional dependencies
Along with the optional dependencies for Manim and ManimGL,
Manim Slides offers additional *extras*, that can be activated
using optional dependencies:
- `full`, to include `magic`, `manim`, 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;
- `pyqt6-full` to include `full` and `pyqt6`;
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
on most platforms and Python version, except on Python 3.12[^2];
- `pyside6-full` to include `full` and `pyside6`;
- `sphinx-directive`, to generate presentation inside your Sphinx
documentation. This automatically installs `manim`,
and does not work with ManimGL;
and does not work with ManimGL.
Installing those extras can be done with the following syntax:
@ -91,14 +115,59 @@ Installing those extras can be done with the following syntax:
pipx install -U "manim-slides[extra1,extra2]"
```
[^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
observe the same visual bug as with PyQt6.
## Nixpkgs installation
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);
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
module (for notebook magics), and includes IPython but does not include
any Qt binding.
You can try out the Manim Slides package with:
```sh
nix-shell -p manim ffmpeg manim-slides
```
or by adding it to your
[configuration file](https://nixos.org/manual/nixos/stable/#sec-package-management).
Alternatively, you can try Manim Slides in a Python environment with:
```sh
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}
The quotes `"` are added because not all shell support unquoted
brackets (e.g., zsh) or commas (e.g., Windows).
Nix does not currently support `manimgl`.
:::
## Install From Repository
## When you need a Qt backend
Before `v5.1`, Manim Slides automatically included PySide6 as
a Qt backend. As only `manim-slides present` and `manim-slides wizard`
command need a graphical library, and installing PySide6 on all platforms
and Python version can be sometimes complicated, Manim Slides chooses
**not to include** any Qt backend.
The use can choose between PySide6 (best) and PyQt6, depending on their
availability and licensing rules.
As of `v5.1`, you **need** to have Qt bindings installed to use
`manim-slides present` or `manim-slides wizard`. The recommended way to
install those are via optional dependencies, as explained above.
## Install from source
An alternative way to install Manim Slides is to clone the git repository,
and install from there: read the
[contributing guide](./contributing/workflow)
and build the package from source. Read the
[contributing guide](/contributing/workflow)
to know how to process.

View File

@ -15,7 +15,6 @@ the slides.
If both modules are present in {py:data}`sys.modules`, then Manim Slides will
prefer using `manim`.
### Usage
The simplest way to use Manim Slides with the correct Manim API is to:

View File

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

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

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

View File

@ -2,7 +2,8 @@
Manim Slides' graphical user interface (GUI) is the *de facto* way to present slides.
If you do not specify one of the commands listed in the [CLI reference](./cli),
If you do not specify one of the commands listed in the
[CLI reference](/reference/cli),
Manim Slides will use **present** by default, which launches a GUI window,
playing your scene(s) like so:
@ -25,7 +26,7 @@ directory, you should not worry about that :-)
## Configuration File
It is possible to configure Manim Slides via a configuration file, even though
this feature is currently limited. You may initiliaze the default configuration
this feature is currently limited. You may initialize the default configuration
file with:
```bash

View File

@ -30,11 +30,11 @@ manim-slides convert --show-config
## Using a Custom Template
The default template used for HTML conversion can be found on
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/data/revealjs_template.html)
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html)
or printed with the `--show-template` option.
If you wish to use another template, you can do so with the
`--use-template FILE` option.
## More about HTML Slides
You can read more about HTML slides in the [sharing](./sharing) section.
You can read more about HTML slides in the [sharing](/reference/sharing) section.

View File

@ -7,6 +7,7 @@ Automatically generated reference for Manim Slides.
api
cli
customize_html
examples
gui
html
@ -15,23 +16,23 @@ sharing
Sphinx Extension <sphinx_extension>
```
[Application Programming Interface](./api): list of classes and methods that may
[Application Programming Interface](/reference/api): list of classes and methods that may
be useful to the end-user.
[Command Line Interface](./cli): list of all commands available using Manim
[Command Line Interface](/reference/cli): list of all commands available using Manim
Slides' executable.
[Examples](./examples): curated list of examples and their output.
[Examples](/reference/examples): curated list of examples and their output.
[Graphical User Interface](./gui): details about the main Manim Slide' feature.
[Graphical User Interface](/reference/gui): details about the main Manim Slide' feature.
[HTML Presentation](./html): an alternative way of presenting your animations.
[HTML Presentation](/reference/html): an alternative way of presenting your animations.
[IPython Magic](./ipython_magic): a magic to render and display Manim Slides inside notebooks.
[IPython Magic](/reference/ipython_magic): a magic to render and display Manim Slides inside notebooks.
+ [Example](./magic_example): example notebook using the magics.
+ [Example](/reference/magic_example): example notebook using the magics.
[Sharing](./sharing): how to share your presentation with others.
[Sharing](/reference/sharing): how to share your presentation with others.
[Sphinx Extension](./sphinx_extension): a Sphinx extension for diplaying Manim Slides animations within your documentation.
[Sphinx Extension](/reference/sphinx_extension): a Sphinx extension for displaying Manim Slides animations within your documentation.

View File

@ -18,6 +18,7 @@
"outputs": [],
"source": [
"from manim import *\n",
"\n",
"from manim_slides import *"
]
},
@ -77,9 +78,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"display_name": ".venv",
"language": "python",
"name": "manim-slides"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
@ -91,7 +92,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.11.8"
}
},
"nbformat": 4,

View File

@ -126,15 +126,21 @@ to use an `iframe`:
</div>
```
<!-- markdown-link-check-disable -->
<!-- see why: https://github.com/tcort/markdown-link-check/discussions/189 -->
The additional code comes from
[this article](https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page)
and it there to preserve the original aspect ratio (16:9).
<!-- markdown-link-check-enable -->
### 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
@ -142,8 +148,8 @@ HTML conversion makes it convenient to play your presentation on a
remote server.
This is how your are able to watch all the examples on this website. If you want
to know how to share your slide with GitHub pages, see the
[workflow file](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/pages.yml).
to know how to share your slide with GitHub pages, check out the
[Manim Slides Starter GitHub repository template](https://github.com/jeertmans/manim-slides-starter).
:::{warning}
Keep in mind that playing large video files over the internet network
@ -161,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__
@ -29,7 +29,7 @@ class Module(ModuleType):
return ModuleType.__getattribute__(self, name)
def __dir__(self) -> List[str]:
def __dir__(self) -> list[str]:
result = list(new_module.__all__)
result.extend(
(

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.0-rc3"
__version__ = "5.5.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

@ -4,7 +4,7 @@ from functools import wraps
from inspect import Parameter, signature
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from typing import Any, Callable, Optional
import rtoml
from pydantic import (
@ -13,11 +13,12 @@ from pydantic import (
FilePath,
PositiveInt,
PrivateAttr,
conset,
field_serializer,
field_validator,
model_validator,
)
from pydantic_extra_types.color import Color
from PySide6.QtCore import Qt
from .logger import logger
@ -25,7 +26,7 @@ Receiver = Callable[..., Any]
class Signal(BaseModel): # type: ignore[misc]
__receivers: List[Receiver] = PrivateAttr(default_factory=list)
__receivers: list[Receiver] = PrivateAttr(default_factory=list)
def connect(self, receiver: Receiver) -> None:
self.__receivers.append(receiver)
@ -38,23 +39,23 @@ class Signal(BaseModel): # type: ignore[misc]
receiver(*args)
def key_id(name: str) -> PositiveInt:
"""Avoid importing Qt too early."""
from qtpy.QtCore import Qt
return getattr(Qt, f"Key_{name}")
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
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
@ -71,21 +72,33 @@ 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 = Key(ids=[Qt.Key_Q], name="QUIT")
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
NEXT: Key = Key(ids=[Qt.Key_Right], name="NEXT")
PREVIOUS: Key = Key(ids=[Qt.Key_Left], name="PREVIOUS")
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
REPLAY: Key = Key(ids=[Qt.Key_R], name="REPLAY")
FULL_SCREEN: Key = Key(ids=[Qt.Key_F], name="TOGGLE FULL SCREEN")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
PLAY_PAUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("Space")], name="PLAY / PAUSE")
)
NEXT: Key = Field(default_factory=lambda: Key(ids=[key_id("Right")], name="NEXT"))
PREVIOUS: Key = Field(
default_factory=lambda: Key(ids=[key_id("Left")], name="PREVIOUS")
)
REVERSE: Key = Field(default_factory=lambda: Key(ids=[key_id("V")], name="REVERSE"))
REPLAY: Key = Field(default_factory=lambda: Key(ids=[key_id("R")], name="REPLAY"))
FULL_SCREEN: Key = Field(
default_factory=lambda: Key(ids=[key_id("F")], name="TOGGLE FULL SCREEN")
)
HIDE_MOUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("H")], name="HIDE / SHOW MOUSE")
)
@model_validator(mode="before")
@classmethod
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
ids: Set[int] = set()
def ids_are_unique_across_keys(cls, values: dict[str, Key]) -> dict[str, Key]:
ids: set[int] = set()
for key in values.values():
if len(ids.intersection(key["ids"])) != 0:
@ -121,7 +134,7 @@ class Keys(BaseModel): # type: ignore[misc]
class Config(BaseModel): # type: ignore[misc]
"""General Manim Slides config."""
keys: Keys = Keys()
keys: Keys = Field(default_factory=Keys)
@classmethod
def from_file(cls, path: Path) -> "Config":
@ -147,6 +160,8 @@ class BaseSlideConfig(BaseModel): # type: ignore
reversed_playback_rate: float = 1.0
notes: str = ""
dedent_notes: bool = True
skip_animations: bool = False
src: Optional[FilePath] = None
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
@ -166,7 +181,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)
@ -180,7 +195,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)
@ -191,14 +206,13 @@ class BaseSlideConfig(BaseModel): # type: ignore
return _wrapper_
@model_validator(mode="after")
@classmethod
def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig"
self,
) -> "BaseSlideConfig":
if base_slide_config.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes)
if self.dedent_notes:
self.notes = dedent(self.notes)
return base_slide_config
return self
class PreSlideConfig(BaseSlideConfig):
@ -217,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")
@ -228,36 +242,33 @@ class PreSlideConfig(BaseSlideConfig):
return v
@model_validator(mode="after")
@classmethod
def start_animation_is_before_end(
cls, pre_slide_config: "PreSlideConfig"
self,
) -> "PreSlideConfig":
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
)
if self.start_animation > self.end_animation:
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return pre_slide_config
return self
@model_validator(mode="after")
@classmethod
def loop_and_auto_next_disallowed(
cls, pre_slide_config: "PreSlideConfig"
def has_src_or_more_than_zero_animations(
self,
) -> "PreSlideConfig":
if pre_slide_config.loop and pre_slide_config.auto_next:
if self.src is not None and self.start_animation != self.end_animation:
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."
"A slide cannot have 'src=...' and more than zero animations at the same time."
)
elif self.src is None and self.start_animation == self.end_animation:
raise ValueError(
"You have to play at least one animation (e.g., 'self.wait()') "
"before pausing. If you want to start paused, use the appropriate "
"command-line option when presenting. "
"IMPORTANT: when using ManimGL, 'self.wait()' is not considered "
"to be an animation, so prefer to directly use 'self.play(...)'."
)
return pre_slide_config
return self
@property
def slides_slice(self) -> slice:
@ -274,12 +285,12 @@ 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]
slides: List[SlideConfig] = Field(min_length=1)
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
slides: list[SlideConfig] = Field(min_length=1)
resolution: tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black"
@classmethod
@ -306,26 +317,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
DEFAULT_CONFIG = Config()

View File

@ -1,19 +1,24 @@
import mimetypes
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import textwrap
import warnings
import webbrowser
from base64 import b64encode
from collections import deque
from enum import Enum
from importlib import resources
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type, Union
from typing import Any, Callable, Optional, Union
import av
import click
import cv2
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
@ -52,7 +56,7 @@ def open_with_default(file: Path) -> None:
def validate_config_option(
ctx: Context, param: Parameter, value: Any
) -> Dict[str, str]:
) -> dict[str, str]:
config = {}
for c_option in value:
@ -79,17 +83,32 @@ def file_to_data_uri(file: Path) -> str:
def get_duration_ms(file: Path) -> float:
"""Read a video and return its duration in milliseconds."""
cap = cv2.VideoCapture(str(file))
fps: int = cap.get(cv2.CAP_PROP_FPS)
frame_count: int = cap.get(cv2.CAP_PROP_FRAME_COUNT)
with av.open(str(file)) as container:
video = container.streams.video[0]
return 1000 * frame_count / fps
return float(1000 * video.duration * video.time_base)
def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
"""Read a image from a video file at a given index."""
with av.open(str(file)) as container:
frames = container.decode(video=0)
if frame_index == FrameIndex.last:
(frame,) = deque(frames, 1)
else:
frame = next(frames)
return frame.to_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."""
@ -103,17 +122,18 @@ 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"]:
def from_string(cls, s: str) -> type["Converter"]:
"""Return the appropriate converter from a string name."""
return {
"html": RevealJS,
"pdf": PDF,
"pptx": PowerPoint,
"zip": HtmlZip,
}[s]
@ -273,50 +293,154 @@ 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
auto_animate_styles: List[str] = Field(
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",
"color",
@ -330,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 = "4.6.1"
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.2.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")
@ -366,45 +544,64 @@ class RevealJS(Converter):
if isinstance(self.template, Path):
return self.template.read_text()
if sys.version_info < (3, 9):
return resources.read_text(templates, "revealjs.html")
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())
revealjs_template = Template(
self.load_template(), trim_blocks=True, lstrip_blocks=True
)
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 != ""
@ -416,44 +613,92 @@ class RevealJS(Converter):
file_to_data_uri=file_to_data_uri,
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."""
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
cap = cv2.VideoCapture(str(file))
if frame_index == FrameIndex.last:
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
ret, frame = cap.read()
cap.release()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return Image.fromarray(frame)
else:
raise ValueError("Failed to read {image_index} image from video file")
images = []
for i, presentation_config in enumerate(self.presentation_configs):
@ -478,18 +723,31 @@ 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: # noqa: C901
def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
prs.slide_width = self.width * 9525
@ -505,7 +763,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")
@ -518,69 +776,84 @@ class PowerPoint(Converter):
nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
return etree.ElementBase.xpath(el, query, namespaces=nsmap)
def save_first_image_from_video_file(file: Path) -> Optional[str]:
cap = cv2.VideoCapture(file.as_posix())
ret, frame = cap.read()
cap.release()
with tempfile.TemporaryDirectory() as directory_name:
directory = Path(directory_name)
frame_number = 0
for i, presentation_config in enumerate(self.presentation_configs):
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
file = slide_config.file
if ret:
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
cv2.imwrite(f.name, frame)
f.close()
return f.name
else:
logger.warn("Failed to read first image from video file")
return None
mime_type = mimetypes.guess_type(file)[0]
for i, presentation_config in enumerate(self.presentation_configs):
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
file = slide_config.file
if self.poster_frame_image is None:
poster_frame_image = str(directory / f"{frame_number}.png")
image = read_image_from_video_file(
file, frame_index=FrameIndex.first
)
image.save(poster_frame_image)
mime_type = mimetypes.guess_type(file)[0]
frame_number += 1
else:
poster_frame_image = str(self.poster_frame_image)
if self.poster_frame_image is None:
poster_frame_image = save_first_image_from_video_file(file)
else:
poster_frame_image = str(self.poster_frame_image)
slide = prs.slides.add_slide(layout)
movie = slide.shapes.add_movie(
str(file),
self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes
slide = prs.slides.add_slide(layout)
movie = slide.shapes.add_movie(
str(file),
self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes
if self.auto_play_media:
auto_play_media(movie, loop=slide_config.loop)
if self.auto_play_media:
auto_play_media(movie, loop=slide_config.loop)
dest.parent.mkdir(parents=True, exist_ok=True)
prs.save(dest)
dest.parent.mkdir(parents=True, exist_ok=True)
prs.save(dest)
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)(
presentation_configs=[PresentationConfig()]
)
for key, value in converter.dict().items():
click.echo(f"{key}: {value!r}")
if doc := getattr(cls, "__doc__", ""):
click.echo(textwrap.dedent(doc))
for key, field in cls.model_fields.items():
if field.is_required():
continue
default = field.get_default(call_default_factory=True)
click.echo(click.style(key, bold=True) + f": {default}")
if description := field.description:
click.secho(textwrap.indent(description, prefix="# "), dim=True)
ctx.exit()
@ -596,18 +869,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()
@ -629,7 +915,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,
@ -639,36 +925,50 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"--open",
"open_result",
is_flag=True,
help="Open the newly created file using the approriate application.",
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",
"config_options",
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. E.g., pass `-cslide_number=true` to display slide numbers.",
help="Configuration options passed to the converter. "
"E.g., pass '-cslide_number=true' to display slide numbers.",
)
@click.option(
"--use-template",
"template",
metavar="FILE",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="Use the template given by FILE instead of default one. To echo the default template, use `--show-template`.",
help="Use the template given by FILE instead of default one. "
"To echo the default template, use '--show-template'.",
)
@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
@verbosity_option
def convert(
scenes: List[str],
scenes: list[str],
folder: Path,
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: Dict[str, str],
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)
@ -679,13 +979,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,
@ -701,7 +1030,7 @@ def convert(
errors = e.errors()
msg = [
f"{len(errors)} error(s) occured with configuration options for '{to}', see below."
f"{len(errors)} error(s) occurred with configuration options for '{to}', see below."
]
for error in errors:

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
-------
@ -117,12 +130,73 @@ directive:
A list of methods, separated by spaces,
that is rendered in a reference block after the source code.
template
A path to the template file to use.
config_options
An unprocessed string of options to pass to ``manim-slides convert``.
Options must be separated with a space, and each option must be
a key, value pair using an equal sign as a separator.
Unlike for the CLI version, you don't need to prepend each option with
``-c``.
E.g., pass ``slide_number=true controls=false``.
By default, ``controls=true`` is set.
Examples
--------
The following code::
.. manim-slides:: MySlide
:hide_source:
:config_options: slide_number=true controls=false
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
text = Text("Hello")
self.wipe([], text)
self.next_slide()
self.play(text.animate.scale(2))
self.next_slide()
self.zoom(text)
Renders as follows:
.. manim-slides:: MySlide
:hide_source:
:config_options: slide_number=true controls=false
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
text = Text("Hello")
self.wipe([], text)
self.next_slide()
self.play(text.animate.scale(2))
self.next_slide()
self.zoom(text)
""" # noqa: D400, D415
from __future__ import annotations
import csv
import itertools as it
import os
import re
import shlex
import sys
from pathlib import Path
from timeit import timeit
@ -198,6 +272,10 @@ class ManimSlidesDirective(Directive):
"ref_classes": lambda arg: process_name_list(arg, "class"),
"ref_functions": lambda arg: process_name_list(arg, "func"),
"ref_methods": lambda arg: process_name_list(arg, "meth"),
"template": lambda arg: Path(arg),
"config_options": lambda arg: dict(
option.split("=") for option in shlex.split(arg)
),
}
final_argument_whitespace = True
@ -205,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()
@ -261,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"]
@ -336,9 +414,20 @@ class ManimSlidesDirective(Directive):
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")
)
RevealJS(presentation_configs=presentation_configs, controls="true").convert_to(
destfile
)
template = self.options.get("template", None)
if template:
template = source_file_name.parents[0].joinpath(template)
config_options = self.options.get("config_options", {})
config_options.setdefault("controls", "true")
RevealJS(
presentation_configs=presentation_configs,
template=template,
**config_options,
).convert_to(destfile)
rendered_template = jinja2.Template(TEMPLATE).render(
clsname=clsname,
@ -366,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}",
],
)
@ -394,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):
@ -144,6 +144,7 @@ class ManimSlidesMagic(Magics): # type: ignore
In case you want to hide the red box containing the output progress bar, the ``progress_bar`` config
option should be set to ``None``. This can also be done by passing ``--progress_bar None`` as a
CLI flag.
"""
if local_ns is None:
local_ns = {}
@ -221,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")
@ -248,9 +261,7 @@ class ManimSlidesMagic(Magics): # type: ignore
)
else:
result = HTML(
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{src}"></iframe></div>""".format(
src=tmpfile.as_posix()
)
f"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{tmpfile.as_posix()}"></iframe></div>"""
)
display(result)

View File

@ -1,23 +1,15 @@
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
from pydantic import ValidationError
from PySide6.QtCore import Qt
from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
from ..qt_utils import qapp
from .player import Player
ASPECT_RATIO_MODES = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
@click.command()
@ -30,7 +22,7 @@ def list_scenes(folder: Path) -> None:
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder: Path) -> List[str]:
def _list_scenes(folder: Path) -> list[str]:
"""List available scenes in given directory."""
scenes = []
@ -41,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}"
)
@ -50,7 +42,7 @@ def _list_scenes(folder: Path) -> List[str]:
return scenes
def prompt_for_scenes(folder: Path) -> List[str]:
def prompt_for_scenes(folder: Path) -> list[str]:
"""Prompt the user to select scenes within a given folder."""
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
@ -62,7 +54,7 @@ def prompt_for_scenes(folder: Path) -> List[str]:
click.echo("Choose number corresponding to desired scene/arguments.")
click.echo("(Use comma separated list for multiple entries)")
def value_proc(value: Optional[str]) -> List[str]:
def value_proc(value: Optional[str]) -> list[str]:
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
if not all(0 < i <= len(scene_choices) for i in indices):
@ -84,8 +76,8 @@ def prompt_for_scenes(folder: Path) -> List[str]:
def get_scenes_presentation_config(
scenes: List[str], folder: Path
) -> List[PresentationConfig]:
scenes: list[str], folder: Path
) -> list[PresentationConfig]:
"""Return a list of presentation configurations based on the user input."""
if len(scenes) == 0:
scenes = prompt_for_scenes(folder)
@ -107,7 +99,7 @@ def get_scenes_presentation_config(
def start_at_callback(
ctx: Context, param: Parameter, values: str
) -> Tuple[Optional[int], ...]:
) -> tuple[Optional[int], ...]:
if values == "(None, None)":
return (None, None)
@ -130,7 +122,8 @@ def start_at_callback(
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 2 arguments are expected but you gave {n_values}, please use commas to separate them",
f"exactly 2 arguments are expected but you gave {n_values}, "
"please use commas to separate them",
ctx=ctx,
param=param,
)
@ -229,13 +222,29 @@ 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",
"info_window_screen_number",
metavar="NUMBER",
type=int,
default=None,
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(
scenes: List[str],
def present( # noqa: C901
scenes: list[str],
config_path: Path,
folder: Path,
start_paused: bool,
@ -244,13 +253,14 @@ def present(
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
start_at: Tuple[Optional[int], Optional[int], Optional[int]],
start_at: tuple[Optional[int], Optional[int], Optional[int]],
start_at_scene_number: int,
start_at_slide_number: int,
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:
"""
Present SCENE(s), one at a time, in order.
@ -283,21 +293,54 @@ def present(
if start_at[1]:
start_at_slide_number = start_at[1]
from qtpy.QtCore import Qt
from qtpy.QtGui import QScreen
from ..qt_utils import qapp
from .player import Player
app = qapp()
app.setApplicationName("Manim Slides")
if screen_number is not None:
screens = app.screens()
def get_screen(number: int) -> Optional[QScreen]:
try:
screen = app.screens()[screen_number]
return screens[number]
except IndexError:
logger.error(
f"Invalid screen number {screen_number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
f"Invalid screen number {number}, "
f"allowed values are from 0 to {len(screens) - 1} (incl.)"
)
screen = None
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 and not should_hide_info_window:
info_window_screen = get_screen(info_window_screen_number)
else:
info_window_screen = None
aspect_ratio_modes = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
player = Player(
config,
presentation_configs,
@ -306,16 +349,17 @@ def present(
skip_all=skip_all,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio_mode=ASPECT_RATIO_MODES[aspect_ratio],
aspect_ratio_mode=aspect_ratio_modes[aspect_ratio],
presentation_index=start_at_scene_number,
slide_index=start_at_slide_number,
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
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

@ -1,12 +1,12 @@
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from typing import Optional
from PySide6.QtCore import Qt, QTimer, QUrl, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from PySide6.QtMultimedia import QMediaPlayer
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtWidgets import (
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer, QVideoFrame
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
@ -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
@ -56,7 +52,7 @@ class Info(QWidget): # type: ignore[misc]
self.video_sink = main_video_widget.videoSink()
left_layout.addWidget(main_video_widget)
# Current slide informations
# Current slide information
self.scene_label = QLabel()
self.slide_label = QLabel()
@ -169,7 +165,7 @@ class Player(QMainWindow): # type: ignore[misc]
def __init__(
self,
config: Config,
presentation_configs: List[PresentationConfig],
presentation_configs: list[PresentationConfig],
*,
start_paused: bool = False,
full_screen: bool = False,
@ -183,6 +179,7 @@ class Player(QMainWindow): # type: ignore[misc]
playback_rate: float = 1.0,
next_terminates_loop: bool = False,
hide_info_window: bool = False,
info_window_screen: Optional[QScreen] = None,
):
super().__init__()
@ -225,12 +222,16 @@ 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()
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
self.setCentralWidget(self.video_widget)
self.media_player = QMediaPlayer(self)
self.media_player.setAudioOutput(self.audio_output)
self.media_player.setVideoOutput(self.video_widget)
self.playback_rate = playback_rate
@ -238,13 +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=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
@ -271,7 +271,7 @@ class Player(QMainWindow): # type: ignore[misc]
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
self.media_player.setLoops(1) # Otherwise looping slides never end
if status == QMediaPlayer.EndOfMedia:
if status == QMediaPlayer.MediaStatus.EndOfMedia:
self.load_next_slide()
self.media_player.mediaStatusChanged.connect(media_status_changed)
@ -280,7 +280,7 @@ class Player(QMainWindow): # type: ignore[misc]
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
if (
status == QMediaPlayer.EndOfMedia
status == QMediaPlayer.MediaStatus.EndOfMedia
and self.current_slide_config.auto_next
):
self.load_next_slide()
@ -314,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()
@ -338,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()
@ -390,7 +390,7 @@ class Player(QMainWindow): # type: ignore[misc]
"""
def load_current_media(self, start_paused: bool = False) -> None:
url = QUrl.fromLocalFile(self.current_file)
url = QUrl.fromLocalFile(str(self.current_file))
self.media_player.setSource(url)
if self.playing_reversed_slide:
@ -463,27 +463,44 @@ 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()
def preview_next_slide(self) -> None:
if slide_config := self.next_slide_config:
url = QUrl.fromLocalFile(slide_config.file)
url = QUrl.fromLocalFile(str(slide_config.file))
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:
@ -493,7 +510,7 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def next(self) -> None:
if self.media_player.playbackState() == QMediaPlayer.PausedState:
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif self.next_terminates_loop and self.media_player.loops() != 1:
position = self.media_player.position()
@ -510,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()
@ -521,17 +541,19 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def play_pause(self) -> None:
state = self.media_player.playbackState()
if state == QMediaPlayer.PausedState:
if state == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif state == QMediaPlayer.PlayingState:
elif state == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
@Slot()
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:
@ -540,11 +562,37 @@ class Player(QMainWindow): # type: ignore[misc]
else:
self.setCursor(Qt.BlankCursor)
@Slot()
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()
@Slot()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
key = event.key()
self.dispatch(key)

View File

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

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:
@ -12,7 +12,6 @@ This is especially useful for two reasons:
import subprocess
import sys
from typing import Tuple
import click
@ -39,7 +38,7 @@ import click
help="If set, use ManimGL renderer.",
)
@click.argument("args", metavar="[RENDERER_ARGS]...", nargs=-1, type=click.UNPROCESSED)
def render(ce: bool, gl: bool, args: Tuple[str, ...]) -> None:
def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
"""
Render SCENE(s) from the input FILE, using the specified renderer.
@ -49,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

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

View File

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

@ -11,7 +11,8 @@ that directly calls ``self.play(Animation(...))``, see
__all__ = ["Wipe", "Zoom"]
from typing import Any, Mapping, Optional, Sequence
from collections.abc import Mapping, Sequence
from typing import Any, Optional
import numpy as np
@ -58,6 +59,7 @@ class Wipe(AnimationGroup): # type: ignore[misc]
self.next_slide()
self.play(Wipe(circle, square, shift=3 * LEFT))
"""
def __init__(
@ -122,6 +124,7 @@ class Zoom(AnimationGroup): # type: ignore[misc]
for i in range(2):
self.play(Zoom(circles[i], circles[i+1]))
self.next_slide()
"""
def __init__(

View File

@ -3,14 +3,13 @@ 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
from typing import (
TYPE_CHECKING,
Any,
MutableMapping,
Sequence,
ValuesView,
)
import numpy as np
@ -34,6 +33,12 @@ 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
max_duration_before_split_reverse: float | None = 4.0
num_processes: int | None = None
def __init__(
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
) -> None:
@ -46,6 +51,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
@ -156,6 +162,7 @@ class BaseSlide:
self.remove_from_canvas("title", "slide_number")
self.wipe(self.mobjects_without_canvas, [])
"""
return self._canvas
@ -171,11 +178,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)
@ -187,8 +206,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
@ -248,6 +271,7 @@ class BaseSlide:
self.next_slide()
self.play(FadeOut(circle))
"""
return self._wait_time_between_slides
@ -256,7 +280,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 +299,62 @@ 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 pathlib.Path src:
An optional path to a video file to include as next slide.
The video will be copied into the output folder, but no rescaling
is applied.
: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.
@ -410,6 +460,7 @@ class BaseSlide:
self.next_slide(notes="Bye bye")
self.zoom(square)
"""
if self._current_animation > self._start_animation:
if self.wait_time_between_slides > 0.0:
@ -425,6 +476,21 @@ class BaseSlide:
self._current_slide += 1
if base_slide_config.src is not None:
self._slides.append(
PreSlideConfig.from_base_slide_config_and_animation_indices(
base_slide_config,
self._current_animation,
self._current_animation,
)
)
base_slide_config = BaseSlideConfig() # default
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
@ -444,11 +510,17 @@ class BaseSlide:
)
)
def _save_slides(self, use_cache: bool = True) -> None:
def _save_slides( # noqa: C901
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()
@ -457,6 +529,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
@ -474,14 +549,25 @@ class BaseSlide:
for pre_slide_config in tqdm(
self._slides,
desc=f"Concatenating animation files to '{scene_files_folder}' and generating reversed animations",
desc=f"Concatenating animations to '{scene_files_folder}' and generating reversed animations",
leave=self._leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar,
unit=" slides",
):
slide_files = files[pre_slide_config.slides_slice]
if pre_slide_config.skip_animations:
continue
if pre_slide_config.src:
slide_files = [pre_slide_config.src]
else:
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}"
@ -491,7 +577,18 @@ 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,
max_segment_duration=self.max_duration_before_split_reverse,
num_processes=self.num_processes,
leave=self._leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar,
)
slides.append(
SlideConfig.from_pre_slide_config_and_files(
@ -515,6 +612,22 @@ 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,
@ -564,6 +677,7 @@ class BaseSlide:
return_animation=True
)
self.play(anim)
"""
from .animation import Wipe
@ -628,6 +742,7 @@ class BaseSlide:
return_animation=True
)
self.play(anim)
"""
from .animation import Zoom

View File

@ -1,7 +1,9 @@
from pathlib import Path
from typing import Any, List, Optional, Tuple
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
@ -9,32 +11,80 @@ from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
"""
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provides necessary tools
for slides rendering.
:param args: Positional arguments passed to scene object.
:param pathlib.Path 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.
:cvar typing.Optional[float] max_duration_before_split_reverse: :data:`4.0`: Maximum duration
before of a video animation before it is reversed by splitting the file into smaller chunks.
Generating reversed animations can require an important amount of
memory (because the whole video needs to be kept in memory),
and splitting the video into multiple chunks usually speeds
up the process (because it can be done in parallel) while taking
less memory.
Set this to :data:`None` to disable splitting the file into chunks.
:cvar typing.Optional[int] num_processes: :data:`None`: Number of processes
to use for parallelizable operations.
If :data:`None`, defaults to :func:`os.process_cpu_count`.
This is currently used when generating reversed animations, and can
increase memory consumption.
"""
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 = config["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"]
def _resolution(self) -> tuple[int, int]:
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]:
def _partial_movie_files(self) -> list[Path]:
# When rendering with -na,b (manim only)
# the animations not in [a,b] will be skipped,
# but animation before a will have a None source file.
@ -56,6 +106,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 +137,41 @@ 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 | self._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]
@ -144,6 +221,7 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
self.next_slide()
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
"""
pass

View File

@ -1,8 +1,7 @@
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Tuple
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"]
def _resolution(self) -> tuple[int, int]:
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
)
]
def _partial_movie_files(self) -> list[Path]:
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,11 +59,15 @@ 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):
CONFIG: ClassVar[Dict[str, Any]] = {
CONFIG: ClassVar[dict[str, Any]] = {
"camera_class": ThreeDCamera,
}
pass

View File

@ -12,89 +12,81 @@
<!-- Theme used for syntax highlighting of code -->
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
<!-- <link rel="stylesheet" href="index.css"> -->
</head>
<body>
<div class="reveal">
<div class="slides">
{%- for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{% for presentation_config in presentation_configs -%}
{%- set outer_loop = loop %}
{% for slide_config in presentation_config.slides %}
{% if one_file %}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% set file = assets_dir / slide_config.file.name %}
{%- endif -%}
<section
data-background-size={{ background_size }}
data-background-color="{{ presentation_config.background_color }}"
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif -%}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}>
{% if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
{%- endfor -%}
{%- endfor -%}
{% else %}
{% 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>
{% endfor %}
{% endfor %}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}
{% if has_notes %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
{% endif %}
<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealMarkdown, RevealNotes ],
{%- endif %}
{% if has_notes %}
/// The list of RevealJS plugins.
plugins: [ RevealMarkdown, RevealNotes ],
{% endif %}
// The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different
// resolutions. Can be specified using percentage units.
width: {{ width }},
height: {{ height }},
// Factor of the display size that should remain empty around
// the content
margin: {{ margin }},
// Bounds for smallest/largest possible scale to apply to content
minScale: {{ min_scale }},
maxScale: {{ max_scale }},
// Display presentation control arrows
controls: {{ controls }},
// Help the user learn the controls by providing hints, for example by
// bouncing the down arrow when they first encounter a vertical slide
controlsTutorial: {{ controls_tutorial }},
// Determines where controls appear, "edges" or "bottom-right"
controlsLayout: {{ controls_layout }},
// Visibility rule for backwards navigation arrows; "faded", "hidden"
// or "visible"
controlsBackArrows: {{ controls_back_arrows }},
// Display a presentation progress bar
progress: {{ progress }},
// Display the page number of the current slide
// - true: Show slide number
// - false: Hide slide number
@ -110,55 +102,43 @@
// object and return an array with one string [slideNumber] or
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
slideNumber: {{ slide_number }},
// Can be used to limit the contexts in which the slide number appears
// - "all": Always show the slide number
// - "print": Only when printing to PDF
// - "speaker": Only in the speaker view
showSlideNumber: {{ show_slide_number }},
// Use 1 based indexing for # links to match slide number (default is zero
// based)
hashOneBasedIndex: {{ hash_one_based_index }},
// Add the current slide number to the URL hash so that reloading the
// page/copying the URL will return you to the same slide
hash: {{ hash }},
// Flags if we should monitor the hash and change slides accordingly
respondToHashChanges: {{ respond_to_hash_changes }},
// 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 }},
// Enable keyboard shortcuts for navigation
keyboard: {{ keyboard }},
// Optional function that blocks keyboard events when retuning false
//
// If you set this to 'focused', we will only capture keyboard events
// for embedded decks when they are in focus
keyboardCondition: {{ keyboard_condition }},
// Disables the default reveal.js slide layout (scaling and centering)
// so that you can use custom CSS layout
disableLayout: {{ disable_layout }},
// Enable the slide overview mode
overview: {{ overview }},
// Vertical centering of slides
center: {{ center }},
// Enables touch navigation on devices with touch input
touch: {{ touch }},
// Loop the presentation
loop: {{ loop }},
// Change the presentation direction to be RTL
rtl: {{ rtl }},
// Changes the behavior of our navigation directions.
//
// "default"
@ -184,160 +164,171 @@
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
// from 1.3 -> 2.3.
navigationMode: {{ navigation_mode }},
// Randomizes the order of slides each time the presentation loads
shuffle: {{ shuffle }},
// Turns fragments on and off globally
fragments: {{ fragments }},
// Flags whether to include the current fragment in the URL,
// so that reloading brings you to the same fragment position
fragmentInURL: {{ fragment_in_url }},
// Flags if the presentation is running in an embedded mode,
// i.e. contained within a limited portion of the screen
embedded: {{ embedded }},
// Flags if we should show a help overlay when the question-mark
// key is pressed
help: {{ help }},
// Flags if it should be possible to pause the presentation (blackout)
pause: {{ pause }},
// Flags if speaker notes should be visible to all viewers
showNotes: {{ show_notes }},
// Global override for autolaying embedded media (video/audio/iframe)
// - null: Media will only autoplay if data-autoplay is present
// - true: All media will autoplay, regardless of individual setting
// - false: No media will autoplay, regardless of individual setting
autoPlayMedia: {{ auto_play_media }},
// Global override for preloading lazy-loaded iframes
// - null: Iframes with data-src AND data-preload will be loaded when within
// the viewDistance, iframes with only data-src will be loaded when visible
// - true: All iframes with data-src will be loaded when within the viewDistance
// - false: All iframes with data-src will be loaded only when visible
preloadIframes: {{ preload_iframes }},
// Can be used to globally disable auto-animation
autoAnimate: {{ auto_animate }},
// Optionally provide a custom element matcher that will be
// used to dictate which elements we can animate between.
autoAnimateMatcher: {{ auto_animate_matcher }},
// Default settings for our auto-animate transitions, can be
// overridden per-slide or per-element via data arguments
autoAnimateEasing: {{ auto_animate_easing }},
autoAnimateDuration: {{ auto_animate_duration }},
autoAnimateUnmatched: {{ auto_animate_unmatched }},
// CSS properties that can be auto-animated. Position & scale
// is matched separately so there's no need to include styles
// like top/right/bottom/left, width/height or margin.
autoAnimateStyles: {{ auto_animate_styles }},
// Controls automatic progression to the next slide
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
// is present on the current slide or fragment
// - 1+: All slides will progress automatically at the given interval
// - false: No auto-sliding, even if data-autoslide is present
autoSlide: {{ auto_slide }},
// Stop auto-sliding after user input
autoSlideStoppable: {{ auto_slide_stoppable }},
// Use this method for navigation when auto-sliding (defaults to navigateNext)
autoSlideMethod: {{ auto_slide_method }},
// Specify the average time in seconds that you think you will spend
// presenting each slide. This is used to show a pacing timer in the
// speaker view
defaultTiming: {{ default_timing }},
// Enable slide navigation via mouse wheel
mouseWheel: {{ mouse_wheel }},
// Opens links in an iframe preview overlay
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
// individually
previewLinks: {{ preview_links }},
// Exposes the reveal.js API through window.postMessage
postMessage: {{ post_message }},
// Dispatches all reveal.js events to the parent window through postMessage
postMessageEvents: {{ post_message_events }},
// Focuses body when page changes visibility to ensure keyboard shortcuts work
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
// Transition style
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
// Transition speed
transitionSpeed: {{ transition_speed }}, // default/fast/slow
// Transition style for full page slide backgrounds
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
// The maximum number of pages a single slide can expand onto when printing
// to PDF, unlimited by default
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
// Prints each fragment on a separate slide
pdfSeparateFragments: {{ pdf_separate_fragments }},
// Offset used to reduce the height of content within exported PDF pages.
// This exists to account for environment differences based on how you
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
// on precisely the total height of the document whereas in-browser
// printing has to end one pixel before.
pdfPageHeightOffset: {{ pdf_page_height_offset }},
// Number of slides away from the current that are visible
viewDistance: {{ view_distance }},
// Number of slides away from the current that are visible on mobile
// devices. It is advisable to set this to a lower number than
// viewDistance in order to save resources.
mobileViewDistance: {{ mobile_view_distance }},
// The display mode that will be used to show slides
display: {{ display }},
// Hide cursor if inactive
hideInactiveCursor: {{ hide_inactive_cursor }},
// Time before the cursor is hidden (in ms)
hideCursorTime: {{ hide_cursor_time }}
});
{% if data_uri %}
// Fix found by @t-fritsch on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
function fixBase64VideoBackground(event) {
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
if (event.currentSlide.getAttribute('data-background-video')) {
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
video = background.querySelector('video'),
sources = video.querySelectorAll('source');
sources.forEach((source, i) => {
const src = source.getAttribute('src');
if(src.match(/^data:video.*;base64$/)) {
const nextSrc = sources[i+1]?.getAttribute('src');
video.setAttribute('src', `${src},${nextSrc}`);
}
});
// Override SPACE to play / pause the video
Reveal.addKeyBinding(
{
keyCode: 32,
key: 'SPACE',
description: 'Play / pause video'
},
() => {
var currentVideos = Reveal.getCurrentSlide().slideBackgroundContentElement.getElementsByTagName("video");
if (currentVideos.length > 0) {
if (currentVideos[0].paused == true) currentVideos[0].play();
else currentVideos[0].pause();
} else {
Reveal.next();
}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
);
{% 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}`);
}
});
}
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 );
{% endif %}
</script>
{% if env['READTHEDOCS'] %}
<style>
readthedocs-flyout, readthedocs-notification {
display: none;
}
</style>
{% endif %}
</body>
</html>

View File

@ -1,40 +1,75 @@
import hashlib
import os
import shutil
import tempfile
from collections.abc import Iterator
from multiprocessing import Pool
from pathlib import Path
from typing import List
from typing import Any, Optional
import av
from tqdm import tqdm
from .logger import logger
def concatenate_video_files(files: List[Path], dest: Path) -> None:
def concatenate_video_files(files: list[Path], dest: Path) -> None:
"""Concatenate multiple video files into one."""
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
f.writelines(f"file '{path.absolute()}'\n" for path in files)
f.close()
if len(files) == 1:
shutil.copy(files[0], dest)
return
input_ = av.open(f.name, options={"safe": "0"}, format="concat")
input_stream = input_.streams.video[0]
output = av.open(str(dest), mode="w")
output_stream = output.add_stream(
template=input_stream,
)
def _filter(files: list[Path]) -> Iterator[Path]:
"""Patch possibly empty video files."""
for file in files:
with av.open(str(file)) as container:
if len(container.streams.video) > 0:
yield file
else:
logger.warning(
f"Skipping video file {file} because it does "
"not contain any video stream. "
"This is probably caused by Manim, see: "
"https://github.com/jeertmans/manim-slides/issues/390."
)
for packet in input_.demux(input_stream):
# We need to skip the "flushing" packets that `demux` generates.
if packet.dts is None:
continue
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.writelines(f"file '{file}'\n" for file in _filter(files))
tmp_file = f.name
# We need to assign the packet to the new stream.
packet.stream = output_stream
output.mux(packet)
with (
av.open(tmp_file, format="concat", options={"safe": "0"}) as input_container,
av.open(str(dest), mode="w") as output_container,
):
input_video_stream = input_container.streams.video[0]
output_video_stream = output_container.add_stream(
template=input_video_stream,
)
input_.close()
output.close()
if len(input_container.streams.audio) > 0:
input_audio_stream = input_container.streams.audio[0]
output_audio_stream = output_container.add_stream(
template=input_audio_stream,
)
for packet in input_container.demux():
if packet.dts is None:
continue
ptype = packet.stream.type
if ptype == "video":
packet.stream = output_video_stream
elif ptype == "audio":
packet.stream = output_audio_stream
else:
continue # We don't support subtitles
output_container.mux(packet)
os.unlink(tmp_file) # https://stackoverflow.com/a/54768241
def merge_basenames(files: List[Path]) -> Path:
def merge_basenames(files: list[Path]) -> Path:
"""Merge multiple filenames by concatenating basenames."""
if len(files) == 0:
raise ValueError("Cannot merge an empty list of files!")
@ -61,38 +96,102 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
c.link_to(n)
def reverse_video_file(src: Path, dest: Path) -> None:
"""Reverses a video file, writting the result to `dest`."""
input_ = av.open(str(src))
input_stream = input_.streams.video[0]
output = av.open(str(dest), mode="w")
output_stream = output.add_stream(codec_name="libx264", rate=input_stream.base_rate)
output_stream.width = input_stream.width
output_stream.height = input_stream.height
output_stream.pix_fmt = input_stream.pix_fmt
def reverse_video_file_in_one_chunk(src_and_dest: tuple[Path, Path]) -> None:
"""Reverses a video file, writing the result to `dest`."""
src, dest = src_and_dest
with (
av.open(str(src)) as input_container,
av.open(str(dest), mode="w") as output_container,
):
input_stream = input_container.streams.video[0]
output_stream = output_container.add_stream(
codec_name="libx264", rate=input_stream.base_rate
)
output_stream.width = input_stream.width
output_stream.height = input_stream.height
output_stream.pix_fmt = input_stream.pix_fmt
graph = av.filter.Graph()
link_nodes(
graph.add_buffer(template=input_stream),
graph.add("reverse"),
graph.add("buffersink"),
)
graph.configure()
graph = av.filter.Graph()
link_nodes(
graph.add_buffer(template=input_stream),
graph.add("reverse"),
graph.add("buffersink"),
)
graph.configure()
frames_count = 0
for frame in input_.decode(video=0):
graph.push(frame)
frames_count += 1
frames_count = 0
for frame in input_container.decode(video=0):
graph.push(frame)
frames_count += 1
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
for _ in range(frames_count):
frame = graph.pull()
frame.pict_type = 1 # Otherwise we get a warning saying it is changed
output.mux(output_stream.encode(frame))
for _ in range(frames_count):
frame = graph.pull()
frame.pict_type = "NONE" # Otherwise we get a warning saying it is changed
output_container.mux(output_stream.encode(frame))
for packet in output_stream.encode():
output.mux(packet)
for packet in output_stream.encode():
output_container.mux(packet)
input_.close()
output.close()
def reverse_video_file(
src: Path,
dest: Path,
max_segment_duration: Optional[float] = 4.0,
num_processes: Optional[int] = None,
**tqdm_kwargs: Any,
) -> None:
"""Reverses a video file, writing the result to `dest`."""
with av.open(str(src)) as input_container: # Fast path if file is short enough
input_stream = input_container.streams.video[0]
if max_segment_duration is None:
return reverse_video_file_in_one_chunk((src, dest))
elif input_stream.duration:
if (
float(input_stream.duration * input_stream.time_base)
<= max_segment_duration
):
return reverse_video_file_in_one_chunk((src, dest))
else: # pragma: no cover
logger.debug(
f"Could not determine duration of {src}, falling back to segmentation."
)
with tempfile.TemporaryDirectory() as tmpdirname:
tmpdir = Path(tmpdirname)
with av.open(
str(tmpdir / f"%04d.{src.suffix}"),
"w",
format="segment",
options={"segment_time": str(max_segment_duration)},
) as output_container:
output_stream = output_container.add_stream(
template=input_stream,
)
for packet in input_container.demux(input_stream):
if packet.dts is None:
continue
packet.stream = output_stream
output_container.mux(packet)
src_files = list(tmpdir.iterdir())
rev_files = [
src_file.with_stem("rev_" + src_file.stem) for src_file in src_files
]
with Pool(num_processes, maxtasksperchild=1) as pool:
for _ in tqdm(
pool.imap_unordered(
reverse_video_file_in_one_chunk, zip(src_files, rev_files)
),
desc="Reversing large file by cutting it in segments",
total=len(src_files),
unit=" files",
**tqdm_kwargs,
):
pass # We just consume the iterator
concatenate_video_files(rev_files[::-1], dest)

View File

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

View File

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

View File

@ -66,7 +66,7 @@ provide new features on a regular basis.
# Easy to Use Commitment
Manim Slides is commited to be an easy-to-use tool, with minimal installation
Manim Slides is committed to be an easy-to-use tool, with minimal installation
procedure and few modifications required. It can either be used locally with its
graphical user interface (GUI), or shared via one of the two formats it can
convert to:
@ -96,13 +96,13 @@ and posted it on YouTube.
# Stability and releases
Manim Slides is continously tested on most recent Python versions, both ManimCE
Manim Slides is continuously tested on most recent Python versions, both ManimCE
and ManimGL, and on all major platforms: **Ubuntu**, **macOS** and **Windows**. Due to Manim
Slide's exposed API being very minimal, and the variety of tests that are
performed, this tool can be considered stable over time.
New releases are very frequent, as they mostly introduce enhancements or small
documention fixes, and the command-line tool automatically notifies for new
documentation fixes, and the command-line tool automatically notifies for new
available updates. Therefore, regularly updating Manim Slides is highly
recommended.
@ -160,7 +160,7 @@ For new feature requests, we highly encourage users to
[create an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
with the appropriate template.
# Acknowledgements
# Acknowledgments
We acknowledge the work of @manim-presentation that paved the initial structure
of Manim Slides with the manim-presentation Python package.

3458
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
[build-system]
build-backend = "pdm.backend"
requires = ["pdm-backend", "setuptools"]
build-backend = "hatchling.build"
requires = ["hatchling", "hatch-fancy-pypi-readme"]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
@ -17,39 +17,66 @@ 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",
"lxml>=4.9.2",
"numpy>=1.19",
"opencv-python>=4.6.0.66",
"pillow>=9.5.0",
"pydantic>=2.0.1",
"pydantic-extra-types>=2.0.0",
"pyside6==6.5.2",
"python-pptx>=0.6.21",
"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.8,<3.12"
requires-python = ">=3.9"
[project.optional-dependencies]
all = [
"manim-slides[magic,manim,manimgl,sphinx-directive]",
docs = [
"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",
]
full = [
"manim-slides[magic,manim,sphinx-directive]",
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.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.6.1,!=6.8.1.1"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
"importlib-metadata>=8.6.1;python_version<'3.10'",
"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"
@ -61,63 +88,123 @@ 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.5.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 = "uv.lock"
[tool.coverage.report]
exclude_lines = [
'pragma: no cover',
'raise NotImplementedError',
'if TYPE_CHECKING:',
'if typing.TYPE_CHECKING:',
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
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"
[tool.mypy]
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
python_version = "3.9"
strict = true
[tool.pdm.dev-dependencies]
dev = [
"bump2version>=1.0.1",
"pre-commit>=3.5.0",
]
docs = [
"manim-slides[magic,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"sphinx>=7.0.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
]
github-action = ["setuptools"]
test = [
"manim-slides[manim,manimgl]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
"pytest-xdist>=3.3.1",
]
[tool.pdm.version]
path = "manim_slides/__version__.py"
source = "file"
[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]
extend-exclude = ["manim_slides/resources.py"]
extend-include = ["*.ipynb"]
line-length = 88
target-version = "py39"
[tool.ruff.lint]
extend-ignore = [
"D100",
"D101",
@ -132,8 +219,16 @@ extend-ignore = [
"D212",
"E501",
]
extend-include = ["*.ipynb"]
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ['manim_slides', 'tests']}
line-length = 88
target-version = "py38"
isort = {known-first-party = ["manim_slides", "tests"]}
[tool.ruff.lint.per-file-ignores]
"docs/source/reference/magic_example.ipynb" = ["F403", "F405"]
"tests/test_slide.py" = ["N801"]
[tool.uv]
dev-dependencies = [
"bump-my-version>=0.20.3",
"pre-commit>=3.5.0",
"setuptools>=73.0.1",
]

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -51,3 +51,28 @@ class ManimSlidesLogo(Scene):
) # order matters
logo.move_to(ORIGIN)
self.add(logo)
class ManimSlidesFavicon(Scene):
def construct(self):
tex_template = TexTemplate()
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
fill_color = "#c9d1d9"
stroke_color = "#343434"
play = Tex(
r"\faStepBackward\faStepForward",
fill_color=fill_color,
stroke_color=stroke_color,
tex_template=tex_template,
).scale(4)
comment = Tex(
r"\reflectbox{\faComment*[regular]}",
fill_color=fill_color,
stroke_color=stroke_color,
tex_template=tex_template,
).scale(9)
comment.move_to(play)
comment.shift(0.4 * DOWN)
favicon = VGroup(comment, play).scale(3)
favicon.move_to(ORIGIN)
self.add(favicon)

5
static/make_favicon.sh Executable file
View File

@ -0,0 +1,5 @@
#! /bin/bash
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 pdm run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py && 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 pdm run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_docs.png
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 pdm run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_github.png
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 pdm run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_light_transparent.png
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 pdm run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_transparent.png
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

@ -1,7 +1,7 @@
import random
import string
from collections.abc import Generator, Iterator
from pathlib import Path
from typing import Generator, Iterator, List
import pytest
@ -65,14 +65,26 @@ def random_path(
@pytest.fixture
def paths() -> Generator[List[Path], None, None]:
def paths() -> Generator[list[Path], None, None]:
random.seed(1234)
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

@ -1,4 +1,4 @@
from typing import MutableMapping
from collections.abc import MutableMapping
import pytest
@ -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 not base_slide._skip_animations
def test_start_and_stop_skip_animations(self, base_slide: BaseSlide) -> None:
base_slide.start_skip_animations()
assert base_slide._skip_animations
base_slide.stop_skip_animations()
assert not base_slide._skip_animations
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,19 +154,173 @@ 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_pdf_converter(
def test_revealjs_offline_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
out_file = tmp_path / "slides.pdf"
PDF(presentation_configs=[presentation_config]).convert_to(out_file)
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()
def test_converter_no_presentation_config(self) -> None:
with pytest.raises(ValidationError):
Converter(presentation_configs=[])
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
) -> None:
out_file = tmp_path / "slides.pdf"
PDF(
presentation_configs=[presentation_config], frame_index=frame_index
).convert_to(out_file)
assert out_file.exists()
def test_pptx_converter(
self, tmp_path: Path, presentation_config: PresentationConfig

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,7 +25,9 @@ def assert_import(
assert slide.MANIMGL == manimgl
@skip_if_py39
def test_force_api() -> None:
pytest.importorskip("manimlib")
import manim # noqa: F401
if "manimlib" in sys.modules:
@ -52,8 +59,10 @@ 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")
import manim # noqa: F401
import manimlib # noqa: F401
@ -77,7 +86,9 @@ def test_manim_imported() -> None:
)
@skip_if_py39
def test_manimgl_imported() -> None:
pytest.importorskip("manimlib")
import manimlib # noqa: F401
if "manim" in sys.modules:

View File

@ -1,9 +1,9 @@
from collections.abc import Iterator
from pathlib import Path
from typing import Iterator, Tuple
import pytest
from click.testing import CliRunner
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
from manim_slides.present import present
@ -11,20 +11,20 @@ from manim_slides.present import present
@pytest.fixture(autouse=True)
def auto_shutdown_qapp() -> Iterator[None]:
if app := QApplication.instance():
app.shutdown()
app.quit()
yield
if app := QApplication.instance():
app.shutdown()
app.quit()
@pytest.fixture(scope="session")
def args(slides_folder: Path) -> Iterator[Tuple[str, ...]]:
def args(slides_folder: Path) -> Iterator[tuple[str, ...]]:
yield ("--folder", str(slides_folder), "--skip-all", "--playback-rate", "25")
def test_present(args: Tuple[str, ...]) -> None:
def test_present(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -34,7 +34,7 @@ def test_present(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_unexisting_slide(args: Tuple[str, ...]) -> None:
def test_present_unexisting_slide(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -44,7 +44,7 @@ def test_present_unexisting_slide(args: Tuple[str, ...]) -> None:
assert "UnexistingSlide.json does not exist" in results.stdout
def test_present_full_screen(args: Tuple[str, ...]) -> None:
def test_present_full_screen(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -54,7 +54,7 @@ def test_present_full_screen(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_hide_mouse(args: Tuple[str, ...]) -> None:
def test_present_hide_mouse(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -64,7 +64,7 @@ def test_present_hide_mouse(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_ignore_aspect_ratio(args: Tuple[str, ...]) -> None:
def test_present_ignore_aspect_ratio(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -76,7 +76,7 @@ def test_present_ignore_aspect_ratio(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_start_at(args: Tuple[str, ...]) -> None:
def test_present_start_at(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -86,7 +86,7 @@ def test_present_start_at(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_start_at_invalid(args: Tuple[str, ...]) -> None:
def test_present_start_at_invalid(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -96,7 +96,7 @@ def test_present_start_at_invalid(args: Tuple[str, ...]) -> None:
assert "Could not set presentation index to 1234"
def test_present_start_at_scene_number(args: Tuple[str, ...]) -> None:
def test_present_start_at_scene_number(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -108,7 +108,7 @@ def test_present_start_at_scene_number(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_start_at_slide_number(args: Tuple[str, ...]) -> None:
def test_present_start_at_slide_number(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -120,7 +120,7 @@ def test_present_start_at_slide_number(args: Tuple[str, ...]) -> None:
assert results.stdout == ""
def test_present_set_screen(args: Tuple[str, ...]) -> None:
def test_present_set_screen(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
@ -131,7 +131,7 @@ def test_present_set_screen(args: Tuple[str, ...]) -> None:
@pytest.mark.skip(reason="Fails when running the whole test suite.")
def test_present_set_invalid_screen(args: Tuple[str, ...]) -> None:
def test_present_set_invalid_screen(args: tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():

View File

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

View File

@ -1,6 +1,12 @@
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 pytest
from click.testing import CliRunner
@ -15,22 +21,58 @@ from manim import (
Dot,
FadeIn,
GrowFromCenter,
Square,
Text,
)
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(
"renderer",
[
"--CE",
"--GL",
pytest.param(
"--GL",
marks=pytest.mark.skipif(
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,
@ -43,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
@ -71,261 +113,562 @@ 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
@assert_renders
class TestMultipleAnimationsInLastSlide(Slide):
"""Check against solution for issue #161."""
@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)
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
@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)
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
@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(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))
@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
@assert_renders
class TestFileTooLong(Slide):
"""Check against solution for issue #123."""
def test_multiple_animations_in_last_slide(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #161."""
def construct(self) -> None:
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
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_split_reverse(self) -> None:
@assert_renders
class _(CESlide):
max_duration_before_split_reverse = 3.0
assert self._base_slide_config.playback_rate == 1.0
def construct(self) -> None:
self.wait(2.0)
for _ in range(3):
self.next_slide()
self.wait(10.0)
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
@assert_renders
class __(CESlide):
max_duration_before_split_reverse = None
assert self._base_slide_config.playback_rate == 2.0
def construct(self) -> None:
self.wait(5.0)
self.next_slide()
self.wait(5.0)
@assert_constructs
class TestReversedPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
def test_file_too_long(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #123."""
self.add(text)
def construct(self) -> None:
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
assert self._base_slide_config.reversed_playback_rate == 1.0
for _ in range(30):
direction = (random.random() - 0.5) * LEFT + (
random.random() - 0.5
) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
def test_loop(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert self._base_slide_config.reversed_playback_rate == 2.0
self.add(text)
@assert_constructs
class TestNotes(Slide):
def construct(self) -> None:
text = Text("Some text")
assert not self._base_slide_config.loop
self.add(text)
self.next_slide(loop=True)
self.play(text.animate.scale(2))
assert self._base_slide_config.notes == ""
assert self._base_slide_config.loop
self.next_slide(notes="test")
self.play(text.animate.scale(2))
self.next_slide(loop=False)
assert self._base_slide_config.notes == "test"
assert not self._base_slide_config.loop
@assert_constructs
class TestWipe(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
def test_auto_next(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
self.add(text)
assert text in self.mobjects
assert bye not in self.mobjects
assert not self._base_slide_config.auto_next
self.wipe([text], [bye])
self.next_slide(auto_next=True)
self.play(text.animate.scale(2))
assert text not in self.mobjects
assert bye in self.mobjects
assert self._base_slide_config.auto_next
@assert_constructs
class TestZoom(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.next_slide(auto_next=False)
self.add(text)
assert not self._base_slide_config.auto_next
assert text in self.mobjects
assert bye not in self.mobjects
def test_loop_and_auto_next_succeeds(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
self.next_slide(loop=True, auto_next=True)
self.play(text.animate.scale(2))
@assert_constructs
class TestPlay(Slide):
def construct(self) -> None:
assert self._current_animation == 0
circle = Circle(color=BLUE)
dot = Dot()
self.next_slide()
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.play(FadeIn(dot))
assert self._current_animation == 2
def test_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
@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
self.add(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
assert self._base_slide_config.playback_rate == 1.0
@assert_constructs
class TestCanvas(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
assert len(self.canvas) == 0
assert self._base_slide_config.playback_rate == 2.0
self.add(text)
def test_reversed_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert len(self.canvas) == 0
self.add(text)
self.add_to_canvas(text=text)
assert self._base_slide_config.reversed_playback_rate == 1.0
assert len(self.canvas) == 1
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
self.add(bye)
assert self._base_slide_config.reversed_playback_rate == 2.0
assert len(self.canvas) == 1
def test_notes(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
self.add(text)
self.remove(text)
assert self._base_slide_config.notes == ""
assert len(self.canvas) == 1
self.next_slide(notes="test")
self.play(text.animate.scale(2))
self.add_to_canvas(bye=bye)
assert self._base_slide_config.notes == "test"
assert len(self.canvas) == 2
def test_wipe(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.remove_from_canvas("text", "bye")
self.add(text)
assert len(self.canvas) == 0
assert text in self.mobjects
assert bye not in self.mobjects
with pytest.raises(KeyError):
self.remove_from_canvas("text")
self.wipe([text], [bye])
assert text not in self.mobjects
assert bye in self.mobjects
def test_zoom(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.add(text)
assert text in self.mobjects
assert bye not in self.mobjects
self.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_next_slide_include_video(self) -> None:
class Foo(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
self.next_slide()
self.wait(2)
with tmp_cwd() as tmp_dir:
init_slide(Foo).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) == 3
class Bar(CESlide):
def construct(self) -> None:
self.next_slide(src=config.slides[0].file)
self.wait(2)
self.next_slide()
self.wait(2)
self.next_slide() # Dummy
self.next_slide(src=config.slides[1].file, loop=True)
self.next_slide() # Dummy
self.wait(2)
self.next_slide(src=config.slides[2].file)
init_slide(Bar).render()
slide_file = slides_folder / "Bar.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 6
assert config.slides[-3].loop
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

@ -1,17 +1,16 @@
from pathlib import Path
from typing import List
from manim_slides.utils import merge_basenames
def test_merge_basenames(paths: List[Path]) -> None:
def test_merge_basenames(paths: list[Path]) -> None:
path = merge_basenames(paths)
assert path.suffix == paths[0].suffix
assert path.parent == paths[0].parent
def test_merge_basenames_same_with_different_parent_directories(
paths: List[Path],
paths: list[Path],
) -> None:
d1 = Path("a/b/c")
d2 = Path("d/e/f")

View File

@ -1,17 +1,18 @@
from pathlib import Path
from click.testing import CliRunner
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QApplication,
QMessageBox,
)
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from manim_slides.config import Config, Key
from manim_slides.defaults import CONFIG_PATH
from manim_slides.wizard import KeyInput, Wizard, init, wizard
from manim_slides.wizard import init, wizard
from manim_slides.wizard.wizard import KeyInput, Wizard
class TestKeyInput:
@ -68,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()
@ -88,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:
@ -105,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:
@ -119,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")
@ -155,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:

4294
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff