Compare commits

...

31 Commits

Author SHA1 Message Date
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
27 changed files with 973 additions and 551 deletions

View File

@ -29,7 +29,7 @@ body:
options: 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; - 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 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 required: true
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html); - label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
required: true required: true
@ -122,7 +122,7 @@ body:
This will be automatically formatted into code, so no need for backticks. This will be automatically formatted into code, so no need for backticks.
placeholder: | placeholder: |
from manim import * from manim import *
from manim_slides.slide import Slide from manim_slides import Slide
class MWE(Slide): class MWE(Slide):

View File

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

View File

@ -1 +1 @@
3.11.8 3.11

View File

@ -2,7 +2,7 @@ version: 2
build: build:
os: ubuntu-22.04 os: ubuntu-22.04
tools: tools:
python: '3.10' python: '3.11'
apt_packages: apt_packages:
- libpango1.0-dev - libpango1.0-dev
- ffmpeg - ffmpeg

View File

@ -8,7 +8,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- start changelog --> <!-- start changelog -->
(unreleased)= (unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.0...HEAD) ## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.0...HEAD)
(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)=
## [v5.3.0](https://github.com/jeertmans/manim-slides/compare/v5.2.0...v5.3.0) ## [v5.3.0](https://github.com/jeertmans/manim-slides/compare/v5.2.0...v5.3.0)
@ -39,17 +118,17 @@ 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. was added to `manim-slides present` to force the info window.
When there are multiple monitors, the info window will no longer When there are multiple monitors, the info window will no longer
be on the same monitor as the main window, unless overridden. 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) [#482](https://github.com/jeertmans/manim-slides/pull/482)
(v5.2.0-chore)= (v5.2.0-chore)=
### 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. with Manim's.
[#499](https://github.com/jeertmans/manim-slides/pull/499) [#499](https://github.com/jeertmans/manim-slides/pull/499)
- Bump ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies, - 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`, and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`,
as the latter includes many breaking changes. as the latter includes many breaking changes.
[#506](https://github.com/jeertmans/manim-slides/pull/506) [#506](https://github.com/jeertmans/manim-slides/pull/506)
@ -70,7 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.1.10-changed)= (v5.1.10-changed)=
### 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) [#488](https://github.com/jeertmans/manim-slides/pull/488)
(v5.1.10-fixed)= (v5.1.10-fixed)=
@ -112,7 +191,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.1.8-chore)= (v5.1.8-chore)=
### 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), see [#398](https://github.com/jeertmans/manim-slides/pull/398),
until until
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74) [samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
@ -143,7 +222,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed whitespace issue in default RevealJS template. - Fixed whitespace issue in default RevealJS template.
[#442](https://github.com/jeertmans/manim-slides/pull/442) [#442](https://github.com/jeertmans/manim-slides/pull/442)
- Fixed black screen issue on recent Qt versions and device loss detected, - 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) [#465](https://github.com/jeertmans/manim-slides/pull/465)
(v5.1.8-removed)= (v5.1.8-removed)=

View File

@ -4,13 +4,14 @@
cff-version: 1.2.0 cff-version: 1.2.0
title: Manim Slides title: Manim Slides
message: >- message: >-
If you use this software, please cite it using the If you use this software, please cite it using our article
metadata from this file. in the Journal of Open Source Education.
type: software type: software
authors: authors:
- name: Jérome Eertmans - name: Jérome Eertmans
orcid: 'https://orcid.org/0000-0002-5579-5360' orcid: 'https://orcid.org/0000-0002-5579-5360'
website: 'https://eertmans.be' website: 'https://eertmans.be'
doi: 10.5281/zenodo.7971360
repository-code: 'https://github.com/jeertmans/manim-slides' repository-code: 'https://github.com/jeertmans/manim-slides'
url: 'https://eertmans.be/manim-slides' url: 'https://eertmans.be/manim-slides'
abstract: >- abstract: >-
@ -26,7 +27,7 @@ keywords:
- PowerPoint - PowerPoint
- Python - Python
license: MIT license: MIT
version: v5.3.0 version: v5.5.0
preferred-citation: preferred-citation:
publisher: publisher:
name: The Open Journal name: The Open Journal

View File

@ -1,9 +1,3 @@
> [!IMPORTANT]
> Take the [**Manim Slides Survey**](https://forms.gle/i4scrwPQghbTQwQs5)
> to help improve this tool! Thanks in advance to all the people taking the time
> to answer this short survey! The form is open until **January 31st 2025**,
> and results will be communicated in the GitHub discussions.
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png"> <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png"> <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
@ -234,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 [pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest [documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
[documentation-url]: https://manim-slides.readthedocs.io/ [documentation-url]: https://manim-slides.readthedocs.io/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg [doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7971360.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167 [doi-url]: https://doi.org/10.5281/zenodo.7971360
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg [jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
[jose-url]: https://doi.org/10.21105/jose.00206 [jose-url]: https://doi.org/10.21105/jose.00206
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4 [codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4

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 # clone and build manim-slides
COPY . /opt/manim-slides COPY . /opt/manim-slides
WORKDIR /opt/manim-slides WORKDIR /opt/manim-slides
ENV UV_PYTHON=/usr/local/bin/python
RUN pip install --no-cache-dir uv RUN pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
RUN uv pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
ARG NB_USER=manimslidesuser ARG NB_USER=manimslidesuser
ARG NB_UID=1000 ARG NB_UID=1000

View File

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

View File

@ -4,6 +4,7 @@
# For the full list of built-in configuration values, see the documentation: # For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html # https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys import sys
from datetime import date from datetime import date
@ -55,6 +56,7 @@ add_module_names = False
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#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_theme = "furo"
html_static_path = ["_static"] html_static_path = ["_static"]
html_favicon = "_static/favicon.png" html_favicon = "_static/favicon.png"

View File

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

View File

@ -53,7 +53,7 @@ class ConvertExample(Slide):
self.next_slide() self.next_slide()
code = Code( code = Code(
code="""from manim import * code_string="""from manim import *
class Example(Scene): class Example(Scene):
@ -72,7 +72,7 @@ class Example(Scene):
) )
code_step_1 = Code( code_step_1 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Scene): class Example(Scene):
@ -91,7 +91,7 @@ class Example(Scene):
) )
code_step_2 = Code( code_step_2 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Slide): class Example(Slide):
@ -110,7 +110,7 @@ class Example(Slide):
) )
code_step_3 = Code( code_step_3 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Slide): class Example(Slide):
@ -129,7 +129,7 @@ class Example(Slide):
) )
code_step_4 = Code( code_step_4 = Code(
code="""from manim import * code_string="""from manim import *
from manim_slides import Slide from manim_slides import Slide
class Example(Slide): class Example(Slide):
@ -148,19 +148,19 @@ class Example(Slide):
) )
code_step_5 = Code( code_step_5 = Code(
code="manim-slide render example.py Example", code_string="manim-slide render example.py Example",
language="console", language="console",
) )
code_step_6 = Code( code_step_6 = Code(
code="manim-slides Example", code_string="manim-slides Example",
language="console", language="console",
) )
or_text = Text("or generate HTML presentation").scale(0.5) or_text = Text("or generate HTML presentation").scale(0.5)
code_step_7 = Code( code_step_7 = Code(
code="manim-slides convert Example slides.html --open", code_string="manim-slides convert Example slides.html --open",
language="console", language="console",
).shift(DOWN) ).shift(DOWN)

View File

@ -1 +1 @@
__version__ = "5.3.0" __version__ = "5.5.0"

View File

@ -160,6 +160,8 @@ class BaseSlideConfig(BaseModel): # type: ignore
reversed_playback_rate: float = 1.0 reversed_playback_rate: float = 1.0
notes: str = "" notes: str = ""
dedent_notes: bool = True dedent_notes: bool = True
skip_animations: bool = False
src: Optional[FilePath] = None
@classmethod @classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]: def wrapper(cls, arg_name: str) -> Callable[..., Any]:
@ -204,14 +206,13 @@ class BaseSlideConfig(BaseModel): # type: ignore
return _wrapper_ return _wrapper_
@model_validator(mode="after") @model_validator(mode="after")
@classmethod
def apply_dedent_notes( def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig" self,
) -> "BaseSlideConfig": ) -> "BaseSlideConfig":
if base_slide_config.dedent_notes: if self.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes) self.notes = dedent(self.notes)
return base_slide_config return self
class PreSlideConfig(BaseSlideConfig): class PreSlideConfig(BaseSlideConfig):
@ -241,25 +242,33 @@ class PreSlideConfig(BaseSlideConfig):
return v return v
@model_validator(mode="after") @model_validator(mode="after")
@classmethod
def start_animation_is_before_end( def start_animation_is_before_end(
cls, pre_slide_config: "PreSlideConfig" self,
) -> "PreSlideConfig": ) -> "PreSlideConfig":
if pre_slide_config.start_animation >= pre_slide_config.end_animation: if self.start_animation > self.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(...)`."
)
raise ValueError( raise ValueError(
"Start animation index must be strictly lower than end animation index" "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 @property
def slides_slice(self) -> slice: def slides_slice(self) -> slice:

View File

@ -531,7 +531,7 @@ class RevealJS(Converter):
"black", "black",
description="Background color used in slides, not relevant if videos fill the whole area.", description="Background color used in slides, not relevant if videos fill the whole area.",
) )
reveal_version: str = Field("5.1.0", description="RevealJS version.") reveal_version: str = Field("5.2.0", description="RevealJS version.")
reveal_theme: RevealTheme = Field( reveal_theme: RevealTheme = Field(
RevealTheme.black, description="RevealJS version." RevealTheme.black, description="RevealJS version."
) )
@ -594,7 +594,9 @@ class RevealJS(Converter):
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "w") as f: 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() options = self.model_dump()

View File

@ -339,7 +339,7 @@ class ManimSlidesDirective(Directive):
ref_block = "" ref_block = ""
if "quality" in self.options: if "quality" in self.options:
quality = f'{self.options["quality"]}_quality' quality = f"{self.options['quality']}_quality"
else: else:
quality = "example_quality" quality = "example_quality"
frame_rate = QUALITIES[quality]["frame_rate"] frame_rate = QUALITIES[quality]["frame_rate"]
@ -483,7 +483,7 @@ def _log_rendering_times(*args):
) )
for row in group: for row in group:
print( # noqa: T201 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 print("") # noqa: T201

View File

@ -310,7 +310,7 @@ def present( # noqa: C901
except IndexError: except IndexError:
logger.error( logger.error(
f"Invalid screen number {number}, " 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 return None

View File

@ -463,13 +463,13 @@ class Player(QMainWindow): # type: ignore[misc]
def presentation_changed_callback(self) -> None: def presentation_changed_callback(self) -> None:
index = self.current_presentation_index index = self.current_presentation_index
count = self.presentations_count 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() @Slot()
def slide_changed_callback(self) -> None: def slide_changed_callback(self) -> None:
index = self.current_slide_index index = self.current_slide_index
count = self.current_slides_count 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.info.slide_notes.setText(self.current_slide_config.notes)
self.preview_next_slide() self.preview_next_slide()

View File

@ -35,7 +35,7 @@ API: str = os.environ.get(MANIM_API, "manim").lower()
if API not in API_NAMES: if API not in API_NAMES:
raise ImportError( 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] API_NAME = API_NAMES[API]

View File

@ -36,6 +36,8 @@ class BaseSlide:
disable_caching: bool = False disable_caching: bool = False
flush_cache: bool = False flush_cache: bool = False
skip_reversing: bool = False skip_reversing: bool = False
max_duration_before_split_reverse: float | None = 4.0
num_processes: int | None = None
def __init__( def __init__(
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
@ -49,6 +51,7 @@ class BaseSlide:
self._start_animation = 0 self._start_animation = 0
self._canvas: MutableMapping[str, Mobject] = {} self._canvas: MutableMapping[str, Mobject] = {}
self._wait_time_between_slides = 0.0 self._wait_time_between_slides = 0.0
self._skip_animations = False
@property @property
@abstractmethod @abstractmethod
@ -277,7 +280,7 @@ class BaseSlide:
self._wait_time_between_slides = max(wait_time, 0.0) self._wait_time_between_slides = max(wait_time, 0.0)
def play(self, *args: Any, **kwargs: Any) -> None: 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] super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1 self._current_animation += 1
@ -299,6 +302,16 @@ class BaseSlide:
Positional arguments passed to Positional arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`, :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used. 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: :param loop:
If set, next slide will be looping. If set, next slide will be looping.
:param auto_next: :param auto_next:
@ -335,6 +348,11 @@ class BaseSlide:
``manim-slides convert --to=pptx``. ``manim-slides convert --to=pptx``.
:param dedent_notes: :param dedent_notes:
If set, apply :func:`textwrap.dedent` to 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: :param kwargs:
Keyword arguments passed to Keyword arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`, :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
@ -458,6 +476,21 @@ class BaseSlide:
self._current_slide += 1 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._base_slide_config = base_slide_config
self._start_animation = self._current_animation self._start_animation = self._current_animation
@ -477,7 +510,7 @@ class BaseSlide:
) )
) )
def _save_slides( def _save_slides( # noqa: C901
self, self,
use_cache: bool = True, use_cache: bool = True,
flush_cache: bool = False, flush_cache: bool = False,
@ -516,14 +549,25 @@ class BaseSlide:
for pre_slide_config in tqdm( for pre_slide_config in tqdm(
self._slides, 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, leave=self._leave_progress_bar,
ascii=True if platform.system() == "Windows" else None, ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar, 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 dst_file = scene_files_folder / file.name
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}" rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
@ -536,7 +580,15 @@ class BaseSlide:
if skip_reversing: if skip_reversing:
rev_file = dst_file rev_file = dst_file
else: 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( slides.append(
SlideConfig.from_pre_slide_config_and_files( SlideConfig.from_pre_slide_config_and_files(
@ -560,6 +612,22 @@ class BaseSlide:
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'" 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( def wipe(
self, self,
*args: Any, *args: Any,

View File

@ -11,26 +11,43 @@ from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc] 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. for slides rendering.
:param args: Positional arguments passed to scene object. :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. :param kwargs: Keyword arguments passed to scene object.
:cvar bool disable_caching: :data:`False`: Whether to disable the use of :cvar bool disable_caching: :data:`False`: Whether to disable the use of
cached animation files. cached animation files.
:cvar bool flush_cache: :data:`False`: Whether to flush the cache. :cvar bool flush_cache: :data:`False`: Whether to flush the cache.
Unlike with Manim, flushing is performed before rendering. Unlike with Manim, flushing is performed before rendering.
:cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations. :cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations.
If set to :data:`False`, and no cached reversed animation If set to :data:`False`, and no cached reversed animation
exists (or caching is disabled) for a given slide, exists (or caching is disabled) for a given slide,
then the reversed animation will be simply the same then the reversed animation will be simply the same
as the original one, i.e., ``rev_file = file``, as the original one, i.e., ``rev_file = file``,
for the current slide config. 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 @property
def _frame_shape(self) -> tuple[float, float]: def _frame_shape(self) -> tuple[float, float]:
if isinstance(self.renderer, OpenGLRenderer): if isinstance(self.renderer, OpenGLRenderer):
@ -89,6 +106,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
def _start_at_animation_number(self) -> Optional[int]: def _start_at_animation_number(self) -> Optional[int]:
return config["from_animation_number"] # type: ignore 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: def next_section(self, *args: Any, **kwargs: Any) -> None:
""" """
Alias to :meth:`next_slide`. Alias to :meth:`next_slide`.
@ -111,7 +137,12 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
base_slide_config: BaseSlideConfig, base_slide_config: BaseSlideConfig,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> 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__( BaseSlide.next_slide.__wrapped__(
self, self,
base_slide_config=base_slide_config, base_slide_config=base_slide_config,

View File

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

View File

@ -1,16 +1,23 @@
import hashlib import hashlib
import os import os
import shutil
import tempfile import tempfile
from collections.abc import Iterator from collections.abc import Iterator
from multiprocessing import Pool
from pathlib import Path from pathlib import Path
from typing import Any, Optional
import av import av
from tqdm import tqdm
from .logger import logger from .logger import logger
def concatenate_video_files(files: list[Path], dest: Path) -> None: def concatenate_video_files(files: list[Path], dest: Path) -> None:
"""Concatenate multiple video files into one.""" """Concatenate multiple video files into one."""
if len(files) == 1:
shutil.copy(files[0], dest)
return
def _filter(files: list[Path]) -> Iterator[Path]: def _filter(files: list[Path]) -> Iterator[Path]:
"""Patch possibly empty video files.""" """Patch possibly empty video files."""
@ -89,8 +96,9 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
c.link_to(n) 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`.""" """Reverses a video file, writing the result to `dest`."""
src, dest = src_and_dest
with ( with (
av.open(str(src)) as input_container, av.open(str(src)) as input_container,
av.open(str(dest), mode="w") as output_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): for _ in range(frames_count):
frame = graph.pull() 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)) output_container.mux(output_stream.encode(frame))
for packet in output_stream.encode(): for packet in output_stream.encode():
output_container.mux(packet) 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

@ -49,6 +49,7 @@ docs = [
"myst-parser>=2.0.0", "myst-parser>=2.0.0",
"nbsphinx>=0.9.2", "nbsphinx>=0.9.2",
"pandoc>=2.3", "pandoc>=2.3",
"pygments<2.19", # See: https://github.com/ManimCommunity/manim/issues/4104
"sphinx>=7.0.1", "sphinx>=7.0.1",
"sphinxcontrib-programoutput>=0.18", "sphinxcontrib-programoutput>=0.18",
"sphinx-design>=0.6.1", "sphinx-design>=0.6.1",
@ -60,7 +61,7 @@ full = [
"manim-slides[magic,manim,sphinx-directive]", "manim-slides[magic,manim,sphinx-directive]",
] ]
magic = ["manim-slides[manim]", "ipython>=8.12.2"] magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.18.0"] manim = ["manim>=0.19"]
manimgl = ["manimgl>=1.7.2"] manimgl = ["manimgl>=1.7.2"]
pyqt6 = ["pyqt6>=6.7.0"] pyqt6 = ["pyqt6>=6.7.0"]
pyqt6-full = ["manim-slides[full,pyqt6]"] pyqt6-full = ["manim-slides[full,pyqt6]"]
@ -68,6 +69,7 @@ pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
pyside6-full = ["manim-slides[full,pyside6]"] pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"] sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [ tests = [
"importlib-metadata>=8.6.1;python_version<'3.10'",
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]", "manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
"pytest>=7.4.0", "pytest>=7.4.0",
"pytest-cov>=4.1.0", "pytest-cov>=4.1.0",
@ -90,7 +92,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
allow_dirty = false allow_dirty = false
commit = true commit = true
commit_args = "" commit_args = ""
current_version = "5.3.0" current_version = "5.5.0"
ignore_missing_version = false ignore_missing_version = false
message = "chore(deps): bump version from {current_version} to {new_version}" 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+))?' parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'

View File

@ -86,6 +86,15 @@ class TestBaseSlide:
assert base_slide.wait_time_between_slides == 0.0 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: def test_play(self) -> None:
pass # This method should be tested in test_slide.py pass # This method should be tested in test_slide.py

View File

@ -1,6 +1,10 @@
import contextlib
import os
import random import random
import shutil import shutil
import sys import sys
import tempfile
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import Any, Union
@ -17,6 +21,7 @@ from manim import (
Dot, Dot,
FadeIn, FadeIn,
GrowFromCenter, GrowFromCenter,
Square,
Text, Text,
) )
from manim.renderer.opengl_renderer import OpenGLRenderer 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.", reason="See https://github.com/3b1b/manim/issues/2263.",
), ),
), ),
"--CE --renderer=opengl",
], ],
ids=("CE", "GL", "CE(GL)"),
) )
def test_render_basic_slide( def test_render_basic_slide(
renderer: str, renderer: str,
@ -78,7 +85,7 @@ def test_render_basic_slide(
with runner.isolated_filesystem() as tmp_dir: with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir) shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke( results = runner.invoke(
render, [renderer, str(slides_file), "BasicSlide", "-ql"] render, [*renderer.split(" "), str(slides_file), "BasicSlide", "-ql"]
) )
assert results.exit_code == 0, results assert results.exit_code == 0, results
@ -229,8 +236,22 @@ def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct() 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: def assert_renders(cls: SlideType) -> None:
init_slide(cls).render() with tmp_cwd():
init_slide(cls).render()
class TestSlide: class TestSlide:
@ -293,6 +314,26 @@ class TestSlide:
self.play(dot.animate.move_to(LEFT)) self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN)) 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: def test_file_too_long(self) -> None:
@assert_renders @assert_renders
class _(CESlide): class _(CESlide):
@ -479,6 +520,120 @@ class TestSlide:
self.next_slide() self.next_slide()
assert self._current_slide == 2 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: def test_canvas(self) -> None:
@assert_constructs @assert_constructs
class _(CESlide): class _(CESlide):

601
uv.lock generated

File diff suppressed because it is too large Load Diff