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:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- label: Checked the [frequently asked questions]](https://manim-slides.eertmans.be/latest/faq.html);
- label: Checked the [frequently asked questions](https://manim-slides.eertmans.be/latest/faq.html);
required: true
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
required: true
@ -122,7 +122,7 @@ body:
This will be automatically formatted into code, so no need for backticks.
placeholder: |
from manim import *
from manim_slides.slide import Slide
from manim_slides import Slide
class MWE(Slide):

View File

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

View File

@ -1 +1 @@
3.11.8
3.11

View File

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

View File

@ -8,7 +8,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.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](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.
When there are multiple monitors, the info window will no longer
be on the same monitor as the main window, unless overridden.
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
[@taibeled](https://github.com/taibeled)
[#482](https://github.com/jeertmans/manim-slides/pull/482)
(v5.2.0-chore)=
### Chore
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies
- Bumped ManimGL to `>=1.7.1`, to remove conflicting dependencies
with Manim's.
[#499](https://github.com/jeertmans/manim-slides/pull/499)
- 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`,
as the latter includes many breaking changes.
[#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)=
### Changed
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
- Allow multiple slide reverses by going backward [@taibeled](https://github.com/taibeled).
[#488](https://github.com/jeertmans/manim-slides/pull/488)
(v5.1.10-fixed)=
@ -112,7 +191,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(v5.1.8-chore)=
### Chore
- Pin `rtoml==0.9.0` on Windows platforms,
- Pinned `rtoml==0.9.0` on Windows platforms,
see [#398](https://github.com/jeertmans/manim-slides/pull/398),
until
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
@ -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.
[#442](https://github.com/jeertmans/manim-slides/pull/442)
- Fixed black screen issue on recent Qt versions and device loss detected,
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
thanks to [@taibeled](https://github.com/taibeled)!
[#465](https://github.com/jeertmans/manim-slides/pull/465)
(v5.1.8-removed)=

View File

@ -4,13 +4,14 @@
cff-version: 1.2.0
title: Manim Slides
message: >-
If you use this software, please cite it using the
metadata from this file.
If you use this software, please cite it using our article
in the Journal of Open Source Education.
type: software
authors:
- name: Jérome Eertmans
orcid: 'https://orcid.org/0000-0002-5579-5360'
website: 'https://eertmans.be'
doi: 10.5281/zenodo.7971360
repository-code: 'https://github.com/jeertmans/manim-slides'
url: 'https://eertmans.be/manim-slides'
abstract: >-
@ -26,7 +27,7 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.3.0
version: v5.5.0
preferred-citation:
publisher:
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>
<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">
@ -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
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
[documentation-url]: https://manim-slides.readthedocs.io/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7971360.svg
[doi-url]: https://doi.org/10.5281/zenodo.7971360
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
[jose-url]: https://doi.org/10.21105/jose.00206
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4

View File

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

View File

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

View File

@ -4,6 +4,7 @@
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
from datetime import date
@ -55,6 +56,7 @@ add_module_names = False
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
html_theme = "furo"
html_static_path = ["_static"]
html_favicon = "_static/favicon.png"

View File

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

View File

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

View File

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

View File

@ -160,6 +160,8 @@ class BaseSlideConfig(BaseModel): # type: ignore
reversed_playback_rate: float = 1.0
notes: str = ""
dedent_notes: bool = True
skip_animations: bool = False
src: Optional[FilePath] = None
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
@ -204,14 +206,13 @@ class BaseSlideConfig(BaseModel): # type: ignore
return _wrapper_
@model_validator(mode="after")
@classmethod
def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig"
self,
) -> "BaseSlideConfig":
if base_slide_config.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes)
if self.dedent_notes:
self.notes = dedent(self.notes)
return base_slide_config
return self
class PreSlideConfig(BaseSlideConfig):
@ -241,25 +242,33 @@ class PreSlideConfig(BaseSlideConfig):
return v
@model_validator(mode="after")
@classmethod
def start_animation_is_before_end(
cls, pre_slide_config: "PreSlideConfig"
self,
) -> "PreSlideConfig":
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) "
"before pausing. If you want to start paused, use the appropriate "
"command-line option when presenting. "
"IMPORTANT: when using ManimGL, `self.wait()` is not considered "
"to be an animation, so prefer to directly use `self.play(...)`."
)
if self.start_animation > self.end_animation:
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return self
return pre_slide_config
@model_validator(mode="after")
def has_src_or_more_than_zero_animations(
self,
) -> "PreSlideConfig":
if self.src is not None and self.start_animation != self.end_animation:
raise ValueError(
"A slide cannot have 'src=...' and more than zero animations at the same time."
)
elif self.src is None and self.start_animation == self.end_animation:
raise ValueError(
"You have to play at least one animation (e.g., 'self.wait()') "
"before pausing. If you want to start paused, use the appropriate "
"command-line option when presenting. "
"IMPORTANT: when using ManimGL, 'self.wait()' is not considered "
"to be an animation, so prefer to directly use 'self.play(...)'."
)
return self
@property
def slides_slice(self) -> slice:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,8 @@ class BaseSlide:
disable_caching: bool = False
flush_cache: bool = False
skip_reversing: bool = False
max_duration_before_split_reverse: float | None = 4.0
num_processes: int | None = None
def __init__(
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
@ -49,6 +51,7 @@ class BaseSlide:
self._start_animation = 0
self._canvas: MutableMapping[str, Mobject] = {}
self._wait_time_between_slides = 0.0
self._skip_animations = False
@property
@abstractmethod
@ -277,7 +280,7 @@ class BaseSlide:
self._wait_time_between_slides = max(wait_time, 0.0)
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload `self.play` and increment animation count."""
"""Overload 'self.play' and increment animation count."""
super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1
@ -299,6 +302,16 @@ class BaseSlide:
Positional arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
:param skip_animations:
Exclude the next slide from the output.
If `manim` is used, this is also passed to :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
which will avoid rendering the corresponding animations.
.. seealso::
:meth:`start_skip_animations`
:meth:`stop_skip_animations`
:param loop:
If set, next slide will be looping.
:param auto_next:
@ -335,6 +348,11 @@ class BaseSlide:
``manim-slides convert --to=pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param pathlib.Path src:
An optional path to a video file to include as next slide.
The video will be copied into the output folder, but no rescaling
is applied.
:param kwargs:
Keyword arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
@ -458,6 +476,21 @@ class BaseSlide:
self._current_slide += 1
if base_slide_config.src is not None:
self._slides.append(
PreSlideConfig.from_base_slide_config_and_animation_indices(
base_slide_config,
self._current_animation,
self._current_animation,
)
)
base_slide_config = BaseSlideConfig() # default
self._current_slide += 1
if self._skip_animations:
base_slide_config.skip_animations = True
self._base_slide_config = base_slide_config
self._start_animation = self._current_animation
@ -477,7 +510,7 @@ class BaseSlide:
)
)
def _save_slides(
def _save_slides( # noqa: C901
self,
use_cache: bool = True,
flush_cache: bool = False,
@ -516,14 +549,25 @@ class BaseSlide:
for pre_slide_config in tqdm(
self._slides,
desc=f"Concatenating animation files to '{scene_files_folder}' and generating reversed animations",
desc=f"Concatenating animations to '{scene_files_folder}' and generating reversed animations",
leave=self._leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar,
unit=" slides",
):
slide_files = files[pre_slide_config.slides_slice]
if pre_slide_config.skip_animations:
continue
if pre_slide_config.src:
slide_files = [pre_slide_config.src]
else:
slide_files = files[pre_slide_config.slides_slice]
file = merge_basenames(slide_files)
try:
file = merge_basenames(slide_files)
except ValueError as e:
raise ValueError(
f"Failed to merge basenames of files for slide: {pre_slide_config!r}"
) from e
dst_file = scene_files_folder / file.name
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
@ -536,7 +580,15 @@ class BaseSlide:
if skip_reversing:
rev_file = dst_file
else:
reverse_video_file(dst_file, rev_file)
reverse_video_file(
dst_file,
rev_file,
max_segment_duration=self.max_duration_before_split_reverse,
num_processes=self.num_processes,
leave=self._leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar,
)
slides.append(
SlideConfig.from_pre_slide_config_and_files(
@ -560,6 +612,22 @@ class BaseSlide:
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
)
def start_skip_animations(self) -> None:
"""
Start skipping animations.
This automatically applies ``skip_animations=True``
to all subsequent calls to :meth:`next_slide`.
This is useful when you want to skip animations from multiple slides in a row,
without having to manually set ``skip_animations=True``.
"""
self._skip_animations = True
def stop_skip_animations(self) -> None:
"""Stop skipping animations."""
self._skip_animations = False
def wipe(
self,
*args: Any,

View File

@ -11,26 +11,43 @@ from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
"""
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provides necessary tools
for slides rendering.
:param args: Positional arguments passed to scene object.
:param output_folder: Where the slide animation files should be written.
:param pathlib.Path output_folder: Where the slide animation files should be written.
:param kwargs: Keyword arguments passed to scene object.
:cvar bool disable_caching: :data:`False`: Whether to disable the use of
cached animation files.
:cvar bool flush_cache: :data:`False`: Whether to flush the cache.
Unlike with Manim, flushing is performed before rendering.
:cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations.
If set to :data:`False`, and no cached reversed animation
exists (or caching is disabled) for a given slide,
then the reversed animation will be simply the same
as the original one, i.e., ``rev_file = file``,
for the current slide config.
:cvar typing.Optional[float] max_duration_before_split_reverse: :data:`4.0`: Maximum duration
before of a video animation before it is reversed by splitting the file into smaller chunks.
Generating reversed animations can require an important amount of
memory (because the whole video needs to be kept in memory),
and splitting the video into multiple chunks usually speeds
up the process (because it can be done in parallel) while taking
less memory.
Set this to :data:`None` to disable splitting the file into chunks.
:cvar typing.Optional[int] num_processes: :data:`None`: Number of processes
to use for parallelizable operations.
If :data:`None`, defaults to :func:`os.process_cpu_count`.
This is currently used when generating reversed animations, and can
increase memory consumption.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
# OpenGL renderer disables 'write_to_movie' by default
# which is required for saving the animations
config["write_to_movie"] = True
super().__init__(*args, **kwargs)
@property
def _frame_shape(self) -> tuple[float, float]:
if isinstance(self.renderer, OpenGLRenderer):
@ -89,6 +106,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
def _start_at_animation_number(self) -> Optional[int]:
return config["from_animation_number"] # type: ignore
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload 'self.play' and increment animation count."""
super().play(*args, **kwargs)
if self._base_slide_config.skip_animations:
# Manim will not render the animations, so we reset the animation
# counter to the previous value
self._current_animation -= 1
def next_section(self, *args: Any, **kwargs: Any) -> None:
"""
Alias to :meth:`next_slide`.
@ -111,7 +137,12 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
base_slide_config: BaseSlideConfig,
**kwargs: Any,
) -> None:
Scene.next_section(self, *args, **kwargs)
Scene.next_section(
self,
*args,
skip_animations=base_slide_config.skip_animations | self._skip_animations,
**kwargs,
)
BaseSlide.next_slide.__wrapped__(
self,
base_slide_config=base_slide_config,

View File

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

View File

@ -1,16 +1,23 @@
import hashlib
import os
import shutil
import tempfile
from collections.abc import Iterator
from multiprocessing import Pool
from pathlib import Path
from typing import Any, Optional
import av
from tqdm import tqdm
from .logger import logger
def concatenate_video_files(files: list[Path], dest: Path) -> None:
"""Concatenate multiple video files into one."""
if len(files) == 1:
shutil.copy(files[0], dest)
return
def _filter(files: list[Path]) -> Iterator[Path]:
"""Patch possibly empty video files."""
@ -89,8 +96,9 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
c.link_to(n)
def reverse_video_file(src: Path, dest: Path) -> None:
def reverse_video_file_in_one_chunk(src_and_dest: tuple[Path, Path]) -> None:
"""Reverses a video file, writing the result to `dest`."""
src, dest = src_and_dest
with (
av.open(str(src)) as input_container,
av.open(str(dest), mode="w") as output_container,
@ -120,8 +128,70 @@ def reverse_video_file(src: Path, dest: Path) -> None:
for _ in range(frames_count):
frame = graph.pull()
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
frame.pict_type = "NONE" # Otherwise we get a warning saying it is changed
output_container.mux(output_stream.encode(frame))
for packet in output_stream.encode():
output_container.mux(packet)
def reverse_video_file(
src: Path,
dest: Path,
max_segment_duration: Optional[float] = 4.0,
num_processes: Optional[int] = None,
**tqdm_kwargs: Any,
) -> None:
"""Reverses a video file, writing the result to `dest`."""
with av.open(str(src)) as input_container: # Fast path if file is short enough
input_stream = input_container.streams.video[0]
if max_segment_duration is None:
return reverse_video_file_in_one_chunk((src, dest))
elif input_stream.duration:
if (
float(input_stream.duration * input_stream.time_base)
<= max_segment_duration
):
return reverse_video_file_in_one_chunk((src, dest))
else: # pragma: no cover
logger.debug(
f"Could not determine duration of {src}, falling back to segmentation."
)
with tempfile.TemporaryDirectory() as tmpdirname:
tmpdir = Path(tmpdirname)
with av.open(
str(tmpdir / f"%04d.{src.suffix}"),
"w",
format="segment",
options={"segment_time": str(max_segment_duration)},
) as output_container:
output_stream = output_container.add_stream(
template=input_stream,
)
for packet in input_container.demux(input_stream):
if packet.dts is None:
continue
packet.stream = output_stream
output_container.mux(packet)
src_files = list(tmpdir.iterdir())
rev_files = [
src_file.with_stem("rev_" + src_file.stem) for src_file in src_files
]
with Pool(num_processes, maxtasksperchild=1) as pool:
for _ in tqdm(
pool.imap_unordered(
reverse_video_file_in_one_chunk, zip(src_files, rev_files)
),
desc="Reversing large file by cutting it in segments",
total=len(src_files),
unit=" files",
**tqdm_kwargs,
):
pass # We just consume the iterator
concatenate_video_files(rev_files[::-1], dest)

View File

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

View File

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

View File

@ -1,6 +1,10 @@
import contextlib
import os
import random
import shutil
import sys
import tempfile
from collections.abc import Iterator
from pathlib import Path
from typing import Any, Union
@ -17,6 +21,7 @@ from manim import (
Dot,
FadeIn,
GrowFromCenter,
Square,
Text,
)
from manim.renderer.opengl_renderer import OpenGLRenderer
@ -65,7 +70,9 @@ Slide = Union[CESlide, _GLSlide, CEGLSlide]
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
"--CE --renderer=opengl",
],
ids=("CE", "GL", "CE(GL)"),
)
def test_render_basic_slide(
renderer: str,
@ -78,7 +85,7 @@ def test_render_basic_slide(
with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke(
render, [renderer, str(slides_file), "BasicSlide", "-ql"]
render, [*renderer.split(" "), str(slides_file), "BasicSlide", "-ql"]
)
assert results.exit_code == 0, results
@ -229,8 +236,22 @@ def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct()
@contextlib.contextmanager
def tmp_cwd() -> Iterator[str]:
cwd = os.getcwd()
tmp_dir = tempfile.mkdtemp()
os.chdir(tmp_dir)
try:
yield tmp_dir
finally:
os.chdir(cwd)
def assert_renders(cls: SlideType) -> None:
init_slide(cls).render()
with tmp_cwd():
init_slide(cls).render()
class TestSlide:
@ -293,6 +314,26 @@ class TestSlide:
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
def test_split_reverse(self) -> None:
@assert_renders
class _(CESlide):
max_duration_before_split_reverse = 3.0
def construct(self) -> None:
self.wait(2.0)
for _ in range(3):
self.next_slide()
self.wait(10.0)
@assert_renders
class __(CESlide):
max_duration_before_split_reverse = None
def construct(self) -> None:
self.wait(5.0)
self.next_slide()
self.wait(5.0)
def test_file_too_long(self) -> None:
@assert_renders
class _(CESlide):
@ -479,6 +520,120 @@ class TestSlide:
self.next_slide()
assert self._current_slide == 2
def test_next_slide_skip_animations(self) -> None:
class Foo(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.next_slide(skip_animations=True)
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert self._base_slide_config.skip_animations
self.next_slide()
assert not self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
class Bar(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.next_slide(skip_animations=False)
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert not self._base_slide_config.skip_animations
self.next_slide()
assert not self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
class Baz(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert not self._base_slide_config.skip_animations
self.start_skip_animations()
self.next_slide()
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
assert self._base_slide_config.skip_animations
self.next_slide()
assert self._base_slide_config.skip_animations
self.play(GrowFromCenter(square))
self.stop_skip_animations()
with tmp_cwd() as tmp_dir:
init_slide(Foo).render()
init_slide(Bar).render()
init_slide(Baz).render()
slides_folder = Path(tmp_dir) / "slides"
assert slides_folder.exists()
slide_file = slides_folder / "Foo.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 2
slide_file = slides_folder / "Bar.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 3
slide_file = slides_folder / "Baz.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 1
def test_next_slide_include_video(self) -> None:
class Foo(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
self.next_slide()
self.wait(2)
with tmp_cwd() as tmp_dir:
init_slide(Foo).render()
slides_folder = Path(tmp_dir) / "slides"
assert slides_folder.exists()
slide_file = slides_folder / "Foo.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 3
class Bar(CESlide):
def construct(self) -> None:
self.next_slide(src=config.slides[0].file)
self.wait(2)
self.next_slide()
self.wait(2)
self.next_slide() # Dummy
self.next_slide(src=config.slides[1].file, loop=True)
self.next_slide() # Dummy
self.wait(2)
self.next_slide(src=config.slides[2].file)
init_slide(Bar).render()
slide_file = slides_folder / "Bar.json"
config = PresentationConfig.from_file(slide_file)
assert len(config.slides) == 6
assert config.slides[-3].loop
def test_canvas(self) -> None:
@assert_constructs
class _(CESlide):

601
uv.lock generated

File diff suppressed because it is too large Load Diff