Compare commits

...

48 Commits

Author SHA1 Message Date
e7e7d5e8fc chore(deps): pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.9)
2025-05-12 18:36:29 +00:00
3c6e2db7db chore(deps): pre-commit autoupdate (#543)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.5 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.5...v0.11.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-29 09:38:26 +02:00
04b0eb5685 feat(lib): propagate manim render exit code (#545)
* feat: propagate `manim render` exit code

* changelog

* test

* fix typo
2025-04-29 09:37:55 +02:00
0c6cd67038 chore(dev): move dev-dependencies inside dependency-groups (#542)
* chore(dev): move dev-dependencies inside dependency-groups

* fix(ci): ci was not broken
2025-04-18 14:15:30 +02:00
a5412a8df2 chore(deps): pre-commit autoupdate (#541)
* chore(deps): pre-commit autoupdate

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

* 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-04-15 12:48:36 +02:00
e9480c9bc7 chore(docs): update features table 2025-04-03 11:19:06 +02:00
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
41 changed files with 2200 additions and 1103 deletions

View File

@ -29,7 +29,7 @@ body:
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);
- 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
@ -122,7 +122,7 @@ body:
This will be automatically formatted into code, so no need for backticks.
placeholder: |
from manim import *
from manim_slides.slide import Slide
from manim_slides import Slide
class MWE(Slide):

View File

@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-13, ubuntu-latest, windows-latest]
pyversion: ['3.9', '3.10', '3.11', '3.12']
extras: [pyside6-full, manimgl]
exclude:
@ -41,7 +41,9 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
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
@ -94,7 +96,7 @@ jobs:
uses: ssciwr/setup-mesa-dist-win@v2
- name: Run pytest
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
run: uv run --python ${{ matrix.pyversion }} --frozen --group tests --no-dev pytest
- name: Upload to codecov.io
uses: codecov/codecov-action@v5

View File

@ -21,18 +21,18 @@ repos:
exclude: poetry.lock
args: [--autofix, --trailing-commas]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
rev: v0.11.9
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.0
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:

View File

@ -1 +1 @@
3.11.8
3.11

View File

@ -2,17 +2,17 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: '3.10'
python: '3.11'
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_create_environment:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs --no-dev --no-cache
sphinx:
builder: html
configuration: docs/source/conf.py
fail_on_warning: true
python:
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -8,7 +8,134 @@ 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.2.0...HEAD)
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.1...HEAD)
(unreleased-added)=
### Added
- `manim-slides render` now exits with the same return code as the one returned by `manim render` or `manimgl`.
[@chrjabs](https://github.com/chrjabs) [#545](https://github.com/jeertmans/manim-slides/pull/545)
(unreleased-chore)=
### Chore
- Moved `docs` and `tests` extras, as well as `dev-dependencies`,
inside groups in `dependency-groups`. This could break existing code
when using one of those extras, but as they were not part of the public API,
we do not consider this to be a **breaking change**.
[#542](https://github.com/jeertmans/manim-slides/pull/542)
(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)
@ -21,16 +148,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
was added to `manim-slides present` to force the info window.
When there are multiple monitors, the info window will no longer
be on the same monitor as the main window, unless overridden.
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
[@taibeled](https://github.com/taibeled)
[#482](https://github.com/jeertmans/manim-slides/pull/482)
(v5.2.0-chore)=
### Chore
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies
- 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)
@ -40,11 +172,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `--offline` option to `manim-slides convert` for offline
HTML presentations.
[#440](https://github.com/jeertmans/manim-slides/pull/440)
- Added documentation to config option to `manim-slides convert`
when using `--show-config`.
[#485](https://github.com/jeertmans/manim-slides/pull/485)
(v5.1.10-changed)=
### Changed
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
- 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)=
@ -86,7 +221,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.1.8-chore)=
### Chore
- Pin `rtoml==0.9.0` on Windows platforms,
- 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)
@ -117,7 +252,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed whitespace issue in default RevealJS template.
[#442](https://github.com/jeertmans/manim-slides/pull/442)
- Fixed black screen issue on recent Qt versions and device loss detected,
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
thanks to [@taibeled](https://github.com/taibeled)!
[#465](https://github.com/jeertmans/manim-slides/pull/465)
(v5.1.8-removed)=

View File

@ -4,13 +4,14 @@
cff-version: 1.2.0
title: Manim Slides
message: >-
If you use this software, please cite it using the
metadata from this file.
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'
doi: 10.5281/zenodo.7971360
repository-code: 'https://github.com/jeertmans/manim-slides'
url: 'https://eertmans.be/manim-slides'
abstract: >-
@ -26,7 +27,7 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.2.0
version: v5.5.1
preferred-citation:
publisher:
name: The Open Journal

View File

@ -228,8 +228,8 @@ you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
[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.8215167.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167
[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

View File

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

View File

@ -31,9 +31,8 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
# clone and build manim-slides
COPY . /opt/manim-slides
WORKDIR /opt/manim-slides
ENV UV_PYTHON=/usr/local/bin/python
RUN pip install --no-cache-dir uv
RUN uv pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
RUN pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
ARG NB_USER=manimslidesuser
ARG NB_UID=1000

View File

@ -18,82 +18,75 @@
<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>
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
{% 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 -%}
{% 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
@ -109,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"
@ -183,159 +164,146 @@
// 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}`);
}
});
}
{% 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}`);
function fixBase64VideoBackground(event) {
// Analyze all slides backgrounds
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
// Get the slide video and its sources for each background
const video = slide.querySelector('video');
if (video) {
setVideoBase64(video);
} else {
// Listen to the creation of the video element
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
if (addedNode.tagName === 'VIDEO') {
setVideoBase64(addedNode);
observer.disconnect(); // Stop observing once the video is handled
}
}
}
}
});
observer.observe(slide, { childList: true, subtree: true });
}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
}
// Setup base64 videos
Reveal.on( 'ready', fixBase64VideoBackground );
{% endif %}
</script>

View File

@ -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,6 +56,7 @@ 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"

View File

@ -32,7 +32,7 @@ and development dependencies. If not already, please install this tool.
With uv, installation becomes straightforward:
```bash
uv sync --all-extras
uv sync
```
:::{note}

View File

@ -102,7 +102,7 @@ Questions related to `manim-slides convert [SCENES]... output.html`.
### I moved my `.html` file and it stopped working
If you did not specify `-cdata_uri=true` when converting,
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
then Manim Slides generated a folder containing all
the video files, in the same folder as the HTML
output. As the path to video files is a relative path,

View File

@ -10,20 +10,22 @@ The following summarizes the different presentation features Manim Slides offers
| :--- | :---: | :---: | :---: | :---: |
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
| Replay slide | Yes | No | No | N/A |
| Pause animation | Yes | No | No | N/A |
| Pause animation | Yes | Yes | No | N/A |
| Play slide in reverse | Yes | No | No | N/A |
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
| Needs Python with Manim Slides installed | Yes | No | No | No
| Requires internet access | No | Yes | No | No |
| Requires internet access | No | Depends[^1] | No | No |
| Auto. play slides | Yes | Yes | Yes | N/A |
| Loops support | Yes | Yes | Yes | N/A |
| Fully customizable | No | Yes (`--use-template` option) | No | No |
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1] | None |
| Works cross-platforms | Yes | Yes | Partly[^1][^2] | Yes |
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^2] | None |
| Works cross-platforms | Yes | Yes | Partly[^2][^3] | Yes |
:::
[^1]: If you encounter a problem where slides do not automatically play or loops do not work,
[^1]: By default, HTML assets are loaded from the internet, but they can be
pre-downloaded and embedded in the HTML file at conversion time.
[^2]: If you encounter a problem where slides do not automatically play or loops do not work,
please
[file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).
[^2]: PowerPoint online does not seem to support automatic playing of videos,
[^3]: PowerPoint online does not seem to support automatic playing of videos,
so you need LibreOffice Impress on Linux platforms.

View File

@ -23,10 +23,10 @@ using Manim Slides presentations.
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
[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).
[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
@ -37,12 +37,12 @@ For example, below are the slides of a seminar he gave titled
height="100%"
allowfullscreen
allow="autoplay"
src="https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html">
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/Manim_Theoretical).
[GitHub repository](https://github.com/PanoPepino/mtheoretical).
### Jérome Eertmans

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

@ -137,9 +137,10 @@ and it there to preserve the original aspect ratio (16:9).
### Sharing ONE HTML file
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
all animations will be data URI encoded, making the HTML a self-contained
presentation file that can be shared on its own.
If you set the `--one-file` flag, all animations will be data URI encoded,
making the HTML a self-contained presentation file that can be shared
on its own. If you also set the `--offline` flag, the JS and CSS files will
be included in the HTML file as well.
### Over the internet

View File

@ -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-slide render 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)

View File

@ -1 +1 @@
__version__ = "5.2.0"
__version__ = "5.5.1"

View File

@ -160,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]:
@ -204,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):
@ -241,25 +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 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(...)`."
)
if self.start_animation > self.end_animation:
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return self
return pre_slide_config
@model_validator(mode="after")
def has_src_or_more_than_zero_animations(
self,
) -> "PreSlideConfig":
if self.src is not None and self.start_animation != self.end_animation:
raise ValueError(
"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 self
@property
def slides_slice(self) -> slice:

View File

@ -4,6 +4,8 @@ import platform
import shutil
import subprocess
import tempfile
import textwrap
import warnings
import webbrowser
from base64 import b64encode
from collections import deque
@ -30,7 +32,6 @@ from pydantic import (
PositiveFloat,
PositiveInt,
ValidationError,
conlist,
)
from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.color import Color
@ -102,9 +103,12 @@ def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
class Converter(BaseModel): # type: ignore
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
assets_dir: str = "{basename}_assets"
template: Optional[Path] = None
presentation_configs: list[PresentationConfig]
assets_dir: str = Field(
"{basename}_assets",
description="Assets folder.\nThis is a template string that accepts 'dirname', 'basename', and 'ext' as variables.\nThose variables are obtained from the output filename.",
)
template: Optional[Path] = Field(None, description="Custom template file to use.")
def convert_to(self, dest: Path) -> None:
"""Convert self, i.e., a list of presentations, into a given format."""
@ -289,52 +293,153 @@ class RevealTheme(str, StrEnum):
class RevealJS(Converter):
"""
RevealJS options.
Please check out https://revealjs.com/config/ for more details.
"""
# Export option:
data_uri: bool = False
one_file: bool = Field(
False, description="Embed all assets (e.g., animations) inside the HTML."
)
offline: bool = Field(
False, description="Download remote assets for offline presentation."
)
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
margin: float = 0.04
min_scale: float = 0.2
max_scale: float = 2.0
width: Union[Str, int] = Field(
Str("100%"), description="Width of the presentation."
)
height: Union[Str, int] = Field(
Str("100%"), description="Height of the presentation."
)
margin: float = Field(0.04, description="Margin to use around the content.")
min_scale: float = Field(
0.2, description="Bound for smallest possible scale to apply to content."
)
max_scale: float = Field(
2.0, description="Bound for large possible scale to apply to content."
)
# Configuration options from RevealJS
controls: JsBool = JsBool.false
controls_tutorial: JsBool = JsBool.true
controls_layout: ControlsLayout = ControlsLayout.bottom_right
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
progress: JsBool = JsBool.false
slide_number: SlideNumber = SlideNumber.false
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
hash_one_based_index: JsBool = JsBool.false
hash: JsBool = JsBool.false
respond_to_hash_changes: JsBool = JsBool.false
history: JsBool = JsBool.false
keyboard: JsBool = JsBool.true
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
disable_layout: JsBool = JsBool.false
overview: JsBool = JsBool.true
center: JsBool = JsBool.true
touch: JsBool = JsBool.true
loop: JsBool = JsBool.false
rtl: JsBool = JsBool.false
navigation_mode: NavigationMode = NavigationMode.default
shuffle: JsBool = JsBool.false
fragments: JsBool = JsBool.true
fragment_in_url: JsBool = JsBool.true
embedded: JsBool = JsBool.false
help: JsBool = JsBool.true
pause: JsBool = JsBool.true
show_notes: JsBool = JsBool.false
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
preload_iframes: PreloadIframes = PreloadIframes.null
auto_animate: JsBool = JsBool.true
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
auto_animate_duration: float = 1.0
auto_animate_unmatched: JsBool = JsBool.true
controls: JsBool = Field(
JsBool.false, description="Display presentation control arrows."
)
controls_tutorial: JsBool = Field(
JsBool.true, description="Help the user learn the controls by providing hints."
)
controls_layout: ControlsLayout = Field(
ControlsLayout.bottom_right, description="Determine where controls appear."
)
controls_back_arrows: ControlsBackArrows = Field(
ControlsBackArrows.faded,
description="Visibility rule for backwards navigation arrows.",
)
progress: JsBool = Field(
JsBool.false, description="Display a presentation progress bar."
)
slide_number: SlideNumber = Field(
SlideNumber.false, description="Display the page number of the current slide."
)
show_slide_number: Union[ShowSlideNumber, Function] = Field(
ShowSlideNumber.all,
description="Can be used to limit the contexts in which the slide number appears.",
)
hash_one_based_index: JsBool = Field(
JsBool.false,
description="Use 1 based indexing for # links to match slide number (default is zero based).",
)
hash: JsBool = Field(
JsBool.false,
description="Add the current slide number to the URL hash so that reloading the page/copying the URL will return you to the same slide.",
)
respond_to_hash_changes: JsBool = Field(
JsBool.false,
description="Flags if we should monitor the hash and change slides accordingly.",
)
jump_to_slide: JsBool = Field(
JsBool.true,
description="Enable support for jump-to-slide navigation shortcuts.",
)
history: JsBool = Field(
JsBool.false,
description="Push each slide change to the browser history. Implies `hash: true`.",
)
keyboard: JsBool = Field(
JsBool.true, description="Enable keyboard shortcuts for navigation."
)
keyboard_condition: Union[KeyboardCondition, Function] = Field(
KeyboardCondition.null,
description="Optional function that blocks keyboard events when retuning false.",
)
disable_layout: JsBool = Field(
JsBool.false,
description="Disable the default reveal.js slide layout (scaling and centering) so that you can use custom CSS layout.",
)
overview: JsBool = Field(JsBool.true, description="Enable the slide overview mode.")
center: JsBool = Field(JsBool.true, description="Vertical centering of slides.")
touch: JsBool = Field(
JsBool.true, description="Enable touch navigation on devices with touch input."
)
loop: JsBool = Field(JsBool.false, description="Loop the presentation.")
rtl: JsBool = Field(
JsBool.false, description="Change the presentation direction to be RTL."
)
navigation_mode: NavigationMode = Field(
NavigationMode.default,
description="Change the behavior of our navigation directions.",
)
shuffle: JsBool = Field(
JsBool.false,
description="Randomize the order of slides each time the presentation loads.",
)
fragments: JsBool = Field(
JsBool.true, description="Turns fragment on and off globally."
)
fragment_in_url: JsBool = Field(
JsBool.true,
description="Flag whether to include the current fragment in the URL, so that reloading brings you to the same fragment position.",
)
embedded: JsBool = Field(
JsBool.false,
description="Flag if the presentation is running in an embedded mode, i.e. contained within a limited portion of the screen.",
)
help: JsBool = Field(
JsBool.true,
description="Flag if we should show a help overlay when the question-mark key is pressed.",
)
pause: JsBool = Field(
JsBool.true,
description="Flag if it should be possible to pause the presentation (blackout).",
)
show_notes: JsBool = Field(
JsBool.false,
description="Flag if speaker notes should be visible to all viewers.",
)
auto_play_media: AutoPlayMedia = Field(
AutoPlayMedia.null,
description="Global override for autolaying embedded media (video/audio/iframe).",
)
preload_iframes: PreloadIframes = Field(
PreloadIframes.null,
description="Global override for preloading lazy-loaded iframes.",
)
auto_animate: JsBool = Field(
JsBool.true, description="Can be used to globally disable auto-animation."
)
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = Field(
AutoAnimateMatcher.null,
description="Optionally provide a custom element matcher that will be used to dictate which elements we can animate between.",
)
auto_animate_easing: AutoAnimateEasing = Field(
AutoAnimateEasing.ease,
description="Default settings for our auto-animate transitions, can be overridden per-slide or per-element via data arguments.",
)
auto_animate_duration: float = Field(
1.0, description="See 'auto_animate_easing' documentation."
)
auto_animate_unmatched: JsBool = Field(
JsBool.true, description="See 'auto_animate_easing' documentation."
)
auto_animate_styles: list[str] = Field(
default_factory=lambda: [
"opacity",
@ -349,34 +454,88 @@ class RevealJS(Converter):
"border-radius",
"outline",
"outline-offset",
]
],
description="CSS properties that can be auto-animated.",
)
auto_slide: AutoSlide = Field(
0, description="Control automatic progression to the next slide."
)
auto_slide_stoppable: JsBool = Field(
JsBool.true, description="Stop auto-sliding after user input."
)
auto_slide_method: Union[AutoSlideMethod, Function] = Field(
AutoSlideMethod.null,
description="Use this method for navigation when auto-sliding (defaults to navigateNext).",
)
default_timing: Union[JsNull, int] = Field(
JsNull.null,
description="Specify the average time in seconds that you think you will spend presenting each slide.",
)
mouse_wheel: JsBool = Field(
JsBool.false, description="Enable slide navigation via mouse wheel."
)
preview_links: JsBool = Field(
JsBool.false, description="Open links in an iframe preview overlay."
)
post_message: JsBool = Field(
JsBool.true, description="Expose the reveal.js API through window.postMessage."
)
post_message_events: JsBool = Field(
JsBool.false,
description="Dispatch all reveal.js events to the parent window through postMessage.",
)
focus_body_on_page_visibility_change: JsBool = Field(
JsBool.true,
description="Focus body when page changes visibility to ensure keyboard shortcuts work.",
)
transition: Transition = Field(Transition.none, description="Transition style.")
transition_speed: TransitionSpeed = Field(
TransitionSpeed.default, description="Transition speed."
)
background_size: BackgroundSize = Field(
BackgroundSize.contain, description="Background size attribute for each video."
) # Not in RevealJS
background_transition: BackgroundTransition = Field(
BackgroundTransition.none,
description="Transition style for full page slide backgrounds.",
)
pdf_max_pages_per_slide: Union[int, str] = Field(
"Number.POSITIVE_INFINITY",
description="The maximum number of pages a single slide can expand onto when printing to PDF, unlimited by default.",
)
pdf_separate_fragments: JsBool = Field(
JsBool.true, description="Print each fragment on a separate slide."
)
pdf_page_height_offset: int = Field(
-1,
description="Offset used to reduce the height of content within exported PDF pages.",
)
view_distance: int = Field(
3, description="Number of slides away from the current that are visible."
)
mobile_view_distance: int = Field(
2,
description="Number of slides away from the current that are visible on mobile devices.",
)
display: Display = Field(
Display.block, description="The display mode that will be used to show slides."
)
hide_inactive_cursor: JsBool = Field(
JsBool.true, description="Hide cursor if inactive."
)
hide_cursor_time: int = Field(
5000, description="Time before the cursor is hidden (in ms)."
)
auto_slide: AutoSlide = 0
auto_slide_stoppable: JsBool = JsBool.true
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
default_timing: Union[JsNull, int] = JsNull.null
mouse_wheel: JsBool = JsBool.false
preview_links: JsBool = JsBool.false
post_message: JsBool = JsBool.true
post_message_events: JsBool = JsBool.false
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
background_transition: BackgroundTransition = BackgroundTransition.none
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
pdf_separate_fragments: JsBool = JsBool.true
pdf_page_height_offset: int = -1
view_distance: int = 3
mobile_view_distance: int = 2
display: Display = Display.block
hide_inactive_cursor: JsBool = JsBool.true
hide_cursor_time: int = 5000
# Appearance options from RevealJS
background_color: Color = "black"
reveal_version: str = "5.1.0"
reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides"
background_color: Color = Field(
"black",
description="Background color used in slides, not relevant if videos fill the whole area.",
)
reveal_version: str = Field("5.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")
@ -404,11 +563,10 @@ class RevealJS(Converter):
)
full_assets_dir = dirname / assets_dir
if not self.data_uri or self.offline:
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.data_uri:
if not self.one_file:
num_presentation_configs = len(self.presentation_configs)
if num_presentation_configs > 1:
@ -427,6 +585,7 @@ class RevealJS(Converter):
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)
@ -435,7 +594,9 @@ class RevealJS(Converter):
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.model_dump()
@ -453,28 +614,50 @@ class RevealJS(Converter):
get_duration_ms=get_duration_ms,
has_notes=has_notes,
env=os.environ,
prefix=prefix if not self.data_uri else None,
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 self.offline:
soup = BeautifulSoup(content, "html.parser")
session = requests.Session()
# 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)
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)
content = str(soup)
f.write(content)
@ -500,10 +683,18 @@ 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 convert_to(self, dest: Path) -> None:
@ -532,12 +723,28 @@ 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 convert_to(self, dest: Path) -> None:
@ -616,20 +823,37 @@ class PowerPoint(Converter):
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a '--show-config' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
def callback(ctx: Context, _param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
if "to" in ctx.params:
to = ctx.params["to"]
cls = Converter.from_string(to)
elif "dest" in ctx.params:
dest = Path(ctx.params["dest"])
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = RevealJS
converter = Converter.from_string(to)
if doc := getattr(cls, "__doc__", ""):
click.echo(textwrap.dedent(doc))
for key, field in converter.model_fields.items():
for key, field in cls.model_fields.items():
if field.is_required():
continue
default = field.get_default(call_default_factory=True)
click.echo(f"{key}: {default}")
click.echo(click.style(key, bold=True) + f": {default}")
if description := field.description:
click.secho(textwrap.indent(description, prefix="# "), dim=True)
ctx.exit()
@ -651,12 +875,25 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
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()
@ -707,6 +944,12 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
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,
@ -725,6 +968,7 @@ def convert(
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)
@ -742,6 +986,28 @@ def convert(
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))

View File

@ -339,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"]
@ -483,7 +483,7 @@ def _log_rendering_times(*args):
)
for row in group:
print( # noqa: T201
f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}"
f"{' ' * (max_file_length)} {row[2].rjust(7)}s {row[1]}"
)
print("") # noqa: T201

View File

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

View File

@ -310,7 +310,7 @@ def present( # noqa: C901
except IndexError:
logger.error(
f"Invalid screen number {number}, "
f"allowed values are from 0 to {len(screens)-1} (incl.)"
f"allowed values are from 0 to {len(screens) - 1} (incl.)"
)
return None

View File

@ -463,13 +463,13 @@ class Player(QMainWindow): # type: ignore[misc]
def presentation_changed_callback(self) -> None:
index = self.current_presentation_index
count = self.presentations_count
self.info.scene_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.scene_label.setText(f"{index + 1:4d}/{count:4<d}")
@Slot()
def slide_changed_callback(self) -> None:
index = self.current_slide_index
count = self.current_slides_count
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.slide_label.setText(f"{index + 1:4d}/{count:4<d}")
self.info.slide_notes.setText(self.current_slide_config.notes)
self.preview_next_slide()

View File

@ -1,7 +1,7 @@
"""
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl [OPTIONS] [ARGS]...``.
``manimgl -w [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
@ -48,6 +48,7 @@ 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])
completed = subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
else:
subprocess.run([sys.executable, "-m", "manim", "render", *args])
completed = subprocess.run([sys.executable, "-m", "manim", "render", *args])
sys.exit(completed.returncode)

View File

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

@ -36,6 +36,8 @@ 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
@ -49,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
@ -277,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
@ -299,6 +302,16 @@ class BaseSlide:
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:
@ -335,6 +348,11 @@ class BaseSlide:
``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 passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
@ -458,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
@ -477,7 +510,7 @@ class BaseSlide:
)
)
def _save_slides(
def _save_slides( # noqa: C901
self,
use_cache: bool = True,
flush_cache: bool = False,
@ -516,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}"
@ -536,7 +580,15 @@ class BaseSlide:
if skip_reversing:
rev_file = dst_file
else:
reverse_video_file(dst_file, rev_file)
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(
@ -560,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,

View File

@ -11,26 +11,43 @@ 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 output_folder: Where the slide animation files should be written.
: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):
@ -89,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`.
@ -111,7 +137,12 @@ 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,

View File

@ -2,7 +2,6 @@ from pathlib import Path
from typing import Any, ClassVar, Optional
from manimlib import Scene, ThreeDCamera
from manimlib.utils.file_ops import get_sorted_integer_files
from .base import BaseSlide
@ -10,14 +9,9 @@ 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(
break_into_partial_movies=True,
write_to_movie=True,
subdivide_output=True,
)
# See: https://github.com/3b1b/manim/issues/2261
if kwargs["file_writer_config"].setdefault("output_directory", ".") == "":
kwargs["file_writer_config"]["output_directory"] = "."
kwargs["preview"] = False # Avoid opening a preview window
super().__init__(*args, **kwargs)
@property
@ -46,16 +40,9 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
@property
def _partial_movie_files(self) -> list[Path]:
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return [
Path(file)
for file in get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
]
partial_movie_directory = self.file_writer.partial_movie_directory
extension = self.file_writer.movie_file_extension
return sorted(partial_movie_directory.glob(f"*{extension}"))
@property
def _show_progress_bar(self) -> bool:

View File

@ -12,7 +12,6 @@
<!-- 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>
@ -20,82 +19,74 @@
<div class="reveal">
<div class="slides">
{% for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{%- set outer_loop = loop %}
{% for slide_config in presentation_config.slides %}
{% if one_file %}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% else %}
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
{%- endif -%}
{% 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 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 != "" -%}
{% if slide_config.notes != "" %}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
{% endif %}
</section>
{%- endfor -%}
{%- endfor -%}
{% 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
@ -111,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"
@ -185,168 +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 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 }}
});
// 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();
}
}
);
{% 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}`);
}
});
}
{% 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}`);
}
});
// Analyze all slides backgrounds
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
// Get the slide video and its sources for each background
const video = slide.querySelector('video');
if (video) {
setVideoBase64(video);
} else {
// Listen to the creation of the video element
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
if (addedNode.tagName === 'VIDEO') {
setVideoBase64(addedNode);
observer.disconnect(); // Stop observing once the video is handled
}
}
}
}
});
observer.observe(slide, { childList: true, subtree: true });
}
}
}
// Setup base64 videos
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{%- endif %}
{% endif %}
</script>
{% if env['READTHEDOCS'] -%}
<style>
readthedocs-flyout, readthedocs-notification {
display: none;
}
</style>
{%- endif %}
{% if env['READTHEDOCS'] %}
<style>
readthedocs-flyout, readthedocs-notification {
display: none;
}
</style>
{% endif %}
</body>
</html>

View File

@ -1,16 +1,23 @@
import hashlib
import os
import shutil
import tempfile
from collections.abc import Iterator
from multiprocessing import Pool
from pathlib import Path
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:
"""Concatenate multiple video files into one."""
if len(files) == 1:
shutil.copy(files[0], dest)
return
def _filter(files: list[Path]) -> Iterator[Path]:
"""Patch possibly empty video files."""
@ -89,8 +96,9 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
c.link_to(n)
def reverse_video_file(src: Path, dest: Path) -> None:
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,
@ -120,8 +128,70 @@ def reverse_video_file(src: Path, dest: Path) -> None:
for _ in range(frames_count):
frame = graph.pull()
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
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_container.mux(packet)
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

@ -2,6 +2,39 @@
build-backend = "hatchling.build"
requires = ["hatchling", "hatch-fancy-pypi-readme"]
[dependency-groups]
dev = [
{include-group = "docs"},
{include-group = "tests"},
"bump-my-version>=0.20.3",
"pre-commit>=3.5.0",
]
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",
]
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",
"setuptools>=73.0.1",
]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [
@ -42,37 +75,17 @@ name = "manim-slides"
requires-python = ">=3.9"
[project.optional-dependencies]
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",
"sphinx>=7.0.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.18.0"]
manimgl = ["manimgl>=1.7.1", "pyrr"]
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"]
pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-missing-modules>=0.1.0",
"pytest-qt>=4.2.0",
]
[project.scripts]
manim-slides = "manim_slides.__main__:cli"
@ -88,7 +101,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
allow_dirty = false
commit = true
commit_args = ""
current_version = "5.2.0"
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+))?'
@ -221,10 +234,3 @@ 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",
]

View File

@ -42,3 +42,8 @@ class BasicSlide(Slide):
class BasicSlideSkipReversing(BasicSlide):
skip_reversing = True
class FailingSlide(Slide):
def construct(self):
self.play("this fails to render")

View File

@ -86,6 +86,15 @@ class TestBaseSlide:
assert base_slide.wait_time_between_slides == 0.0
def test_skip_animations(self, base_slide: BaseSlide) -> None:
assert 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

View File

@ -3,7 +3,8 @@ 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 (
@ -174,6 +175,101 @@ class TestConverter:
]:
assert (assets_dir / file).exists()
def test_revealjs_data_encode(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Mock requests.Session.get to return a fake response (should not be called)
class MockResponse:
def __init__(self, content: bytes, text: str, status_code: int) -> None:
self.content = content
self.text = text
self.status_code = status_code
# Apply the monkeypatch
monkeypatch.setattr(
requests.Session,
"get",
lambda self, url: MockResponse(
b"body { background-color: #9a3241; }",
"body { background-color: #9a3241; }",
200,
),
)
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[presentation_config], offline="false", one_file="true"
).convert_to(out_file)
assert out_file.exists()
# Check that assets are not stored
assert not (tmp_path / "slides_assets").exists()
with open(out_file, encoding="utf-8") as file:
content = file.read()
soup = BeautifulSoup(content, "html.parser")
# Check if video is encoded in base64
videos = soup.find_all("section")
assert all(
"data:video/mp4;base64," in video["data-background-video"]
for video in videos
)
# Check if CSS is not inlined
styles = soup.find_all("style")
assert not any("background-color: #9a3241;" in style.string for style in styles)
# Check if JS is not inlined
scripts = soup.find_all("script")
assert not any(
"background-color: #9a3241;" in (script.string or "") for script in scripts
)
def test_revealjs_offline_inlining(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Mock requests.Session.get to return a fake response
class MockResponse:
def __init__(self, content: bytes, text: str, status_code: int) -> None:
self.content = content
self.text = text
self.status_code = status_code
# Apply the monkeypatch
monkeypatch.setattr(
requests.Session,
"get",
lambda self, url: MockResponse(
b"body { background-color: #9a3241; }",
"body { background-color: #9a3241; }",
200,
),
)
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[presentation_config], offline="true", one_file="true"
).convert_to(out_file)
assert out_file.exists()
with open(out_file, encoding="utf-8") as file:
content = file.read()
soup = BeautifulSoup(content, "html.parser")
# Check if CSS is inlined
styles = soup.find_all("style")
assert any("background-color: #9a3241;" in style.string for style in styles)
# Check if JS is inlined
scripts = soup.find_all("script")
assert any("background-color: #9a3241;" in script.string for script in scripts)
def test_htmlzip_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
@ -226,10 +322,6 @@ class TestConverter:
).convert_to(out_file)
assert out_file.exists()
def test_converter_no_presentation_config(self) -> None:
with pytest.raises(ValidationError):
Converter(presentation_configs=[])
def test_pptx_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:

View File

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

View File

@ -1,6 +1,10 @@
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
@ -17,6 +21,7 @@ from manim import (
Dot,
FadeIn,
GrowFromCenter,
Square,
Text,
)
from manim.renderer.opengl_renderer import OpenGLRenderer
@ -65,7 +70,9 @@ Slide = Union[CESlide, _GLSlide, CEGLSlide]
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
"--CE --renderer=opengl",
],
ids=("CE", "GL", "CE(GL)"),
)
def test_render_basic_slide(
renderer: str,
@ -78,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
@ -106,6 +113,37 @@ def test_render_basic_slide(
assert local_presentation_config.resolution == presentation_config.resolution
@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.",
),
),
"--CE --renderer=opengl",
],
ids=("CE", "GL", "CE(GL)"),
)
def test_render_failing_slide(
renderer: str,
slides_file: Path,
manimgl_config: Path,
) -> None:
runner = CliRunner()
with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke(
render, [*renderer.split(" "), str(slides_file), "FailingSlide", "-ql"]
)
assert results.exit_code != 0, results
def test_clear_cache(
slides_file: Path,
) -> None:
@ -212,13 +250,10 @@ def init_slide(cls: SlideType) -> Slide:
if issubclass(cls, CESlide):
return cls()
elif issubclass(cls, GLSlide):
from manimlib.config import get_configuration, parse_cli
from manimlib.extract_scene import get_scene_config
from manimlib.config import parse_cli
args = parse_cli()
config = get_configuration(args)
scene_config = get_scene_config(config)
return cls(**scene_config)
_args = parse_cli()
return cls()
raise ValueError(f"Unsupported class {cls}")
@ -232,8 +267,22 @@ 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:
init_slide(cls).render()
with tmp_cwd():
init_slide(cls).render()
class TestSlide:
@ -296,6 +345,26 @@ class TestSlide:
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
def test_split_reverse(self) -> None:
@assert_renders
class _(CESlide):
max_duration_before_split_reverse = 3.0
def construct(self) -> None:
self.wait(2.0)
for _ in range(3):
self.next_slide()
self.wait(10.0)
@assert_renders
class __(CESlide):
max_duration_before_split_reverse = None
def construct(self) -> None:
self.wait(5.0)
self.next_slide()
self.wait(5.0)
def test_file_too_long(self) -> None:
@assert_renders
class _(CESlide):
@ -482,6 +551,120 @@ class TestSlide:
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):

1454
uv.lock generated

File diff suppressed because it is too large Load Diff