Compare commits

...

27 Commits

Author SHA1 Message Date
880cba3064 chore(lib): moved Sphinx directive from docs to sphinxext 2025-04-18 18:55:47 +02:00
fbf5f42f31 Merge branch 'main' into reorganize 2025-04-18 14:17:07 +02:00
0c6cd67038 chore(dev): move dev-dependencies inside dependency-groups (#542)
* chore(dev): move dev-dependencies inside dependency-groups

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

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

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-15 12:48:36 +02:00
e9480c9bc7 chore(docs): update features table 2025-04-03 11:19:06 +02:00
3e0268a431 chore(deps): bump version from 5.5.0 to 5.5.1 2025-03-28 15:26:55 +01:00
6e14dc9051 feat(html): pause HTML slides with SPACE key (#539)
* feat(html): pause HTML slides with `SPACE` key

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

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-28 15:25:53 +01:00
4a400398b8 chore(deps): pre-commit autoupdate (#537)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2)

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

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

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-04 15:27:42 +01:00
3c8384a908 chore(fmt): auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-29 18:29:09 +00:00
4bf4d38fd8 Merge branch 'main' into reorganize 2025-01-29 19:28:57 +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
f02ef630cf chore(lib): pyproject.toml config 2024-08-26 09:42:22 +02:00
ae8d5b6aab chore(lib): re-organizing the module
This PR refactors a lot of the code in order to have a better organized codebase. If will that I needed to create a second level of submodules, to better distinguish the different parts of this project.
2024-08-20 11:31:58 +02:00
48 changed files with 951 additions and 491 deletions

View File

@ -96,7 +96,7 @@ jobs:
uses: ssciwr/setup-mesa-dist-win@v2
- name: Run pytest
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
run: uv run --python ${{ matrix.pyversion }} --frozen --group tests --no-dev pytest
- name: Upload to codecov.io
uses: codecov/codecov-action@v5

View File

@ -21,18 +21,18 @@ repos:
exclude: poetry.lock
args: [--autofix, --trailing-commas]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.2
rev: v0.11.5
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.1
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

@ -6,13 +6,13 @@ build:
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_create_environment:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs --no-dev --no-cache
sphinx:
builder: html
configuration: docs/source/conf.py
fail_on_warning: true
python:
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -8,7 +8,62 @@ 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.4.2...HEAD)
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.1...HEAD)
(unreleased-chore)=
### Chore
- Moved `docs` and `tests` extras, as well as `dev-dependencies`,
inside groups in `dependency-groups`. This could break existing code
when using one of those extras, but as they were not part of the public API,
we do not consider this to be a **breaking change**.
[#542](https://github.com/jeertmans/manim-slides/pull/542)
- Moved `manim_slides.docs.manim_slides_directive` to `manim_slides.sphinxext.manim_slides_directive`.
This is a **breaking change** because documentation configs have
to be updated.
[#242](https://github.com/jeertmans/manim-slides/pull/242)
(v5.5.1)=
## [v5.5.1](https://github.com/jeertmans/manim-slides/compare/v5.5.0...v5.5.1)
(v5.5.1-changed)=
### Changed
- HTML template now always includes the *notes* plugin so that the speaker
view is always available. Previously, it was only included if the slides
had notes.
[#538](https://github.com/jeertmans/manim-slides/pull/538)
- Pressing <kbd>SPACE</kbd> key now pauses the slides, instead of skipping it.
Previously, it was not possible to pause HTML slides, which can be very annoying
when trying to explain something.
[#539](https://github.com/jeertmans/manim-slides/pull/539)
(v5.5.0)=
## [v5.5.0](https://github.com/jeertmans/manim-slides/compare/v5.4.2...v5.5.0)
(v5.5.0-added)=
### Added
- Added `max_duration_before_split_reverse` and `num_processes` class variables.
[#439](https://github.com/jeertmans/manim-slides/pull/439)
- Added `src = ...` filepath argument to allow inserting external
videos as slides.
[#526](https://github.com/jeertmans/manim-slides/pull/526)
(v5.5.0-changed)=
### Changed
- Automatically split large video animations into smaller chunks
for lightweight (and potentially faster) reversed animations generation.
[#439](https://github.com/jeertmans/manim-slides/pull/439)
(v5.5.0-chore)=
### Chore
- Trimmed whitespaces in HTML template.
[#443](https://github.com/jeertmans/manim-slides/pull/443)
- Bumped RevealJS' version to 5.2 to allow video playing in speaker view.
[#536](https://github.com/jeertmans/manim-slides/pull/536)
(v5.4.2)=
## [v5.4.2](https://github.com/jeertmans/manim-slides/compare/v5.4.1...v5.4.2)
@ -91,7 +146,7 @@ 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)=
@ -122,7 +177,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)=
@ -195,7 +250,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.4.2
version: v5.5.1
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

@ -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,138 +164,104 @@
// 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.

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
@ -36,7 +37,7 @@ extensions = [
"sphinx_copybutton",
"sphinx_design",
# Custom
"manim_slides.docs.manim_slides_directive",
"manim_slides.sphinxext.manim_slides_directive",
]
autodoc_typehints = "both"
@ -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

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

View File

@ -92,7 +92,7 @@ This will also show the default value for each option.
If you want to create your own template, the best is to start from the default one.
You can either download it from the
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/templates)
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/cli/convert/templates)
or use the `manim-slides convert --to=FORMAT --show-template` command,
where `FORMAT` is one of the supported formats.

View File

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

View File

@ -4,7 +4,7 @@ This page contains an exhaustive list of all the commands available with `manim-
```{eval-rst}
.. click:: manim_slides.__main__:cli
.. click:: manim_slides.cli.commands:main
:prog: manim-slides
:nested: full
```

View File

@ -4,7 +4,7 @@ One of the benefits of the `convert` command is the use of template files.
Currently, only the HTML export uses one. If not specified, the template
will be the one shipped with Manim Slides, see
[`manim_slides/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html).
[`manim_slides/cli/convert/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/cli/convert/templates/revealjs.html).
Because you can actually use your own template with the `--use-template`
option, possibilities are infinite!

View File

@ -30,7 +30,7 @@ manim-slides convert --show-config
## Using a Custom Template
The default template used for HTML conversion can be found on
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html)
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/cli/convert/templates/revealjs.html)
or printed with the `--show-template` option.
If you wish to use another template, you can do so with the
`--use-template FILE` option.

View File

@ -1,6 +1,6 @@
# Manim Slides' Sphinx directive
```{eval-rst}
.. automodule:: manim_slides.docs.manim_slides_directive
.. automodule:: manim_slides.sphinxext.manim_slides_directive
:members: ManimSlidesDirective
```

View File

@ -1,3 +1,10 @@
"""
Manim Slides module.
Submodules are lazily imported, in order to provide a faster import experience
in some cases.
"""
import sys
from types import ModuleType
from typing import Any
@ -8,9 +15,7 @@ from .__version__ import __version__
class Module(ModuleType):
def __getattr__(self, name: str) -> Any:
if name == "Slide" or name == "ThreeDSlide":
module = __import__(
"manim_slides.slide", None, None, ["Slide", "ThreeDSlide"]
)
module = __import__("manim_slides.slide", None, None, [name])
return getattr(module, name)
elif name == "ManimSlidesMagic":
module = __import__(

View File

@ -1,11 +1,8 @@
import json
import click
import requests
from click_default_group import DefaultGroup
"""Manim Slides' main entrypoint."""
from .__version__ import __version__
from .checkhealth import checkhealth
from .cli.commands import main
from .convert import convert
from .logger import logger
from .present import list_scenes, present
@ -72,4 +69,4 @@ cli.add_command(render)
cli.add_command(wizard)
if __name__ == "__main__":
cli()
main()

View File

@ -1 +1,3 @@
"""Manim Slides' version."""
__version__ = "5.4.2"

View File

@ -0,0 +1,72 @@
"""Manim Slides' CLI."""
import json
import click
import requests
from click_default_group import DefaultGroup
from ..__version__ import __version__
from ..core.logger import logger
from .convert.commands import convert
from .present.commands import list_scenes, present
from .render.commands import render
from .wizard.commands import init, wizard
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
@click.option(
"--notify-outdated-version/--silent",
" /-S",
is_flag=True,
default=True,
help="Check if a new version of Manim Slides is available.",
)
@click.version_option(__version__, "-v", "--version")
@click.help_option("-h", "--help")
def main(notify_outdated_version: bool) -> None:
"""
Manim Slides command-line utilities.
If no command is specified, defaults to `present`.
"""
# Code below is mostly a copy from:
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
if notify_outdated_version:
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
try:
req_info: requests.models.Response = requests.get(manim_info_url, timeout=2)
req_info.raise_for_status()
stable = req_info.json()["info"]["version"]
if stable != __version__:
click.echo(
"You are using Manim Slides version "
+ click.style(f"v{__version__}", fg="red")
+ ", but version "
+ click.style(f"v{stable}", fg="green")
+ " is available."
)
click.echo(
"You should consider upgrading via "
+ click.style("pip install -U manim-slides", fg="yellow")
)
except requests.exceptions.HTTPError:
logger.debug(f"HTTP Error: {warn_prompt}")
except requests.exceptions.ConnectionError:
logger.debug(f"Connection Error: {warn_prompt}")
except requests.exceptions.Timeout:
logger.debug(f"Timed Out: {warn_prompt}")
except json.JSONDecodeError:
logger.debug(warn_prompt)
logger.debug(f"Error decoding JSON from {manim_info_url}")
except Exception:
logger.debug(f"Something went wrong: {warn_prompt}")
main.add_command(convert)
main.add_command(init)
main.add_command(list_scenes)
main.add_command(present)
main.add_command(render)
main.add_command(wizard)

View File

@ -4,8 +4,9 @@ from typing import Any, Callable
import click
from click import Context, Parameter
from .defaults import CONFIG_PATH, FOLDER_PATH
from .logger import logger
from ..core.config import list_presentation_configs
from ..core.defaults import CONFIG_PATH, FOLDER_PATH
from ..core.logger import logger
F = Callable[..., Any]
Wrapper = Callable[[F], F]
@ -88,6 +89,68 @@ def folder_path_option(function: F) -> F:
callback=callback,
help="Set slides folder.",
show_default=True,
is_eager=True, # Needed to expose its value to other callbacks
)
return wrapper(function)
def scenes_argument(function: F) -> F:
"""
Wrap a function to add a scenes arguments.
This function assumes that :func:`folder_path_option` is also used
on the same decorated function.
"""
def callback(ctx: Context, param: Parameter, value: tuple[str]) -> list[Path]:
folder: Path = ctx.params.get("folder")
presentation_config_paths = list_presentation_configs(folder)
scene_names = [path.stem for path in presentation_config_paths]
num_scenes = len(scene_names)
num_digits = len(str(num_scenes))
if num_scenes == 0:
raise click.UsageError(
f"Folder {folder} does not contain "
"any valid config file, did you render the animations first?"
)
paths = []
if value:
for scene_name in value:
try:
i = scene_names.index(scene_name)
paths.append(presentation_config_paths[i])
except ValueError:
raise click.UsageError(
f"Could not find scene `{scene_name}` in: "
+ ", ".join(scene_names)
+ ". Did you make a typo or forgot to render the animations first?"
) from None
else:
click.echo(
"Choose at least one or more scenes from "
"(enter the corresponding number):\n"
+ "\n".join(
f"- {i:{num_digits}d}: {name}"
for i, name in enumerate(scene_names, start=1)
)
)
continue_prompt = True
while continue_prompt:
index = click.prompt(
"Please enter a value", type=click.IntRange(1, num_scenes)
)
paths.append(presentation_config_paths[index - 1])
continue_prompt = click.confirm(
"Do you want to enter an additional scene?"
)
return paths
wrapper: Wrapper = click.argument("scenes", nargs=-1, callback=callback)
return wrapper(function)

View File

@ -37,14 +37,18 @@ from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.color import Color
from tqdm import tqdm
from ...core.config import PresentationConfig
from ...core.logger import logger
from ..commons import folder_path_option, scenes_argument, verbosity_option
from . import templates
from .commons import folder_path_option, verbosity_option
from .config import PresentationConfig
from .logger import logger
from .present import get_scenes_presentation_config
def open_with_default(file: Path) -> None:
"""
Open a file with the default application.
:param file: The file to open.
"""
system = platform.system()
if system == "Darwin":
subprocess.call(("open", str(file)))
@ -142,6 +146,7 @@ class Str(str):
# This fixes pickling issue on Python 3.8
__reduce_ex__ = str.__reduce_ex__
# TODO: do we still need this?
@classmethod
def __get_pydantic_core_schema__(
@ -531,7 +536,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."
)
@ -547,6 +552,11 @@ class RevealJS(Converter):
return resources.files(templates).joinpath("revealjs.html").read_text()
def open(self, file: Path) -> None:
"""
Open the HTML file inside a web browser.
:param path: The path to the HTML file.
"""
webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> None: # noqa: C901
@ -594,7 +604,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()
@ -908,7 +920,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
@click.command()
@click.argument("scenes", nargs=-1)
@scenes_argument
@folder_path_option
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
@click.option(
@ -958,7 +970,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
@show_config_options
@verbosity_option
def convert(
scenes: list[str],
scenes: list[Path],
folder: Path,
dest: Path,
to: str,
@ -969,7 +981,7 @@ def convert(
one_file: bool,
) -> None:
"""Convert SCENE(s) into a given format and writes the result in DEST."""
presentation_configs = get_scenes_presentation_config(scenes, folder)
presentation_configs = [PresentationConfig.from_file(scene) for scene in scenes]
try:
if to == "auto":

View File

@ -0,0 +1 @@
"""Manim Slides conversion templates."""

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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}
{% if has_notes %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
{% endif %}
<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealMarkdown, RevealNotes ],
{%- endif %}
{% if has_notes %}
/// The list of RevealJS plugins.
plugins: [ RevealMarkdown, RevealNotes ],
{% endif %}
// The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different
// resolutions. Can be specified using percentage units.
width: {{ width }},
height: {{ height }},
// Factor of the display size that should remain empty around
// the content
margin: {{ margin }},
// Bounds for smallest/largest possible scale to apply to content
minScale: {{ min_scale }},
maxScale: {{ max_scale }},
// Display presentation control arrows
controls: {{ controls }},
// Help the user learn the controls by providing hints, for example by
// bouncing the down arrow when they first encounter a vertical slide
controlsTutorial: {{ controls_tutorial }},
// Determines where controls appear, "edges" or "bottom-right"
controlsLayout: {{ controls_layout }},
// Visibility rule for backwards navigation arrows; "faded", "hidden"
// or "visible"
controlsBackArrows: {{ controls_back_arrows }},
// Display a presentation progress bar
progress: {{ progress }},
// Display the page number of the current slide
// - true: Show slide number
// - false: Hide slide number
@ -111,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,139 +164,122 @@
// 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 -%}
// Override SPACE to play / pause the video
Reveal.addKeyBinding(
{
keyCode: 32,
key: 'SPACE',
description: 'Play / pause video'
},
() => {
var currentVideos = Reveal.getCurrentSlide().slideBackgroundContentElement.getElementsByTagName("video");
if (currentVideos.length > 0) {
if (currentVideos[0].paused == true) currentVideos[0].play();
else currentVideos[0].pause();
} else {
Reveal.next();
}
}
);
{% if one_file %}
// Fix found by @t-fritsch and @Rapsssito on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
function setVideoBase64(video) {
@ -362,15 +321,14 @@
}
// 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

@ -0,0 +1 @@
"""Manim Slides' presentation commands."""

View File

@ -7,9 +7,14 @@ import click
from click import Context, Parameter
from pydantic import ValidationError
from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
from ...core.config import Config, PresentationConfig, list_presentation_configs
from ...core.logger import logger
from ..commons import (
config_path_option,
folder_path_option,
scenes_argument,
verbosity_option,
)
@click.command()
@ -18,8 +23,10 @@ from ..logger import logger
@verbosity_option
def list_scenes(folder: Path) -> None:
"""List available scenes."""
for i, scene in enumerate(_list_scenes(folder), start=1):
click.secho(f"{i}: {scene}", fg="green")
scene_names = [path.stem for path in list_presentation_configs(folder)]
num_digits = len(str(len(scene_names)))
for i, scene_name in enumerate(scene_names, start=1):
click.secho(f"{i:{num_digits}d}: {scene_name}", fg="green")
def _list_scenes(folder: Path) -> list[str]:
@ -130,7 +137,7 @@ def start_at_callback(
@click.command()
@click.argument("scenes", nargs=-1)
@scenes_argument
@config_path_option
@folder_path_option
@click.option("--start-paused", is_flag=True, help="Start paused.")
@ -276,7 +283,7 @@ def present( # noqa: C901
if skip_all:
exit_after_last_slide = True
presentation_configs = get_scenes_presentation_config(scenes, folder)
presentation_configs = [PresentationConfig.from_file(path) for path in scenes]
if config_path.exists():
try:

View File

@ -1,14 +1,4 @@
"""
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl -w [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
1. You can are sure to execute the rendering command with the same Python environment
as for ``manim-slides``.
2. You can pass options to the config.
"""
"""Manim Slides' rendering commands."""
import subprocess
import sys
@ -44,10 +34,22 @@ def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
Use ``manim-slides render --help`` to see help information for
a specific renderer.
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
1. You can are sure to execute the rendering command with the same Python environment
as for ``manim-slides``.
2. You can pass options to the config.
"""
if ce and gl:
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
if gl:
subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
else:
subprocess.run([sys.executable, "-m", "manim", "render", *args])
from manim.cli.render.commands import render as render_ce
render_ce(args, standalone_mode=False)

View File

@ -0,0 +1 @@
"""Manim Slides' wizard."""

View File

@ -3,10 +3,9 @@ from pathlib import Path
import click
from ...core.config import Config
from ...core.logger import logger
from ..commons import config_options, verbosity_option
from ..config import Config
from ..defaults import CONFIG_PATH
from ..logger import logger
@click.command()
@ -37,7 +36,7 @@ def _init(
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
logger.debug(f"The `{config_path}` configuration file exists")
if not force and not merge:
choice = click.prompt(
@ -57,7 +56,7 @@ def _init(
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
logger.debug(f"Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
@ -82,4 +81,4 @@ def _init(
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")
logger.debug(f"Configuration file successfully saved to `{config_path}`")

View File

@ -1,3 +1,5 @@
"""Manim Slides' configuration tools."""
import json
import shutil
from functools import wraps
@ -13,6 +15,7 @@ from pydantic import (
FilePath,
PositiveInt,
PrivateAttr,
ValidationError,
conset,
field_serializer,
field_validator,
@ -26,28 +29,54 @@ Receiver = Callable[..., Any]
class Signal(BaseModel): # type: ignore[misc]
__receivers: list[Receiver] = PrivateAttr(default_factory=list)
"""Signal that notifies a list of receivers when it is emitted."""
__receivers: set[Receiver] = PrivateAttr(default_factory=set)
def connect(self, receiver: Receiver) -> None:
self.__receivers.append(receiver)
"""
Connect a receiver to this signal.
This is a no-op if the receiver was already connected to this signal.
:param receiver: The receiver to connect.
"""
self.__receivers.add(receiver)
def disconnect(self, receiver: Receiver) -> None:
self.__receivers.remove(receiver)
"""
Disconnect a receiver from this signal.
This is a no-op if the receiver was not connected to this signal.
:param receiver: The receiver to disconnect.
"""
self.__receivers.discard(receiver)
def emit(self, *args: Any) -> None:
"""
Emit this signal and call each of the attached receivers.
:param args: Positional arguments passed to each receiver.
"""
for receiver in self.__receivers:
receiver(*args)
def key_id(name: str) -> PositiveInt:
"""Avoid importing Qt too early."""
from qtpy.QtCore import Qt
"""
Return the id corresponding to the given key name.
:param str: The name of the key, e.g., 'Q'.
:return: The corresponding id.
"""
from qtpy.QtCore import Qt # Avoid importing Qt too early."""
return getattr(Qt, f"Key_{name}")
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
"""Represent a list of key codes, with optionally a name."""
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
name: Optional[str] = None
@ -58,6 +87,7 @@ class Key(BaseModel): # type: ignore[misc]
self.ids = set(ids)
def match(self, key_id: int) -> bool:
"""Return whether a given key id matches this key."""
m = key_id in self.ids
if m:
@ -135,6 +165,7 @@ class Config(BaseModel): # type: ignore[misc]
"""General Manim Slides config."""
keys: Keys = Field(default_factory=Keys)
"""The key mapping."""
@classmethod
def from_file(cls, path: Path) -> "Config":
@ -142,11 +173,16 @@ class Config(BaseModel): # type: ignore[misc]
return cls.model_validate(rtoml.load(path)) # type: ignore
def to_file(self, path: Path) -> None:
"""Dump the configuration to a file."""
"""Dump this configuration to a file."""
rtoml.dump(self.model_dump(), path, pretty=True)
def merge_with(self, other: "Config") -> "Config":
"""Merge with another config."""
"""
Merge with another config.
:param other: The other config to be merged with.
:return: This config, updated.
"""
self.keys = self.keys.merge_with(other.keys)
return self
@ -155,12 +191,19 @@ class BaseSlideConfig(BaseModel): # type: ignore
"""Base class for slide config."""
loop: bool = False
"""Whether this slide should loop."""
auto_next: bool = False
"""Whether this slide is skipped upon completion."""
playback_rate: float = 1.0
"""The speed at which the animation is played (1.0 is normal)."""
reversed_playback_rate: float = 1.0
"""The speed at which the reversed animation is played."""
notes: str = ""
"""The notes attached to this slide."""
dedent_notes: bool = True
"""Whether to automatically remove any leading indentation in the notes."""
skip_animations: bool = False
src: Optional[FilePath] = None
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
@ -172,7 +215,11 @@ class BaseSlideConfig(BaseModel): # type: ignore
The wrapped function must follow two criteria:
- its last parameter must be ``**kwargs`` (or equivalent);
- and its second last parameter must be ``<arg_name>``.
:param arg_name: The name of the argument.
:return: The wrapped function.
"""
# TODO: improve docs and (maybe) type-hints too
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
@wraps(fun)
@ -205,21 +252,27 @@ 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)
"""
Remove indentation from notes, if specified.
return base_slide_config
:return: The config, optionally modified.
"""
if self.dedent_notes:
self.notes = dedent(self.notes)
return self
class PreSlideConfig(BaseSlideConfig):
"""Slide config to be used prior to rendering."""
start_animation: int
"""The index of the first animation."""
end_animation: int
"""The index after the last animation."""
@classmethod
def from_base_slide_config_and_animation_indices(
@ -228,6 +281,13 @@ class PreSlideConfig(BaseSlideConfig):
start_animation: int,
end_animation: int,
) -> "PreSlideConfig":
"""
Create a config from a base config and animation indices.
:param base_slide_config: The base config.
:param start_animation: The index of the first animation.
:param end_animation: The index after the last animation.
"""
return cls(
start_animation=start_animation,
end_animation=end_animation,
@ -237,30 +297,48 @@ class PreSlideConfig(BaseSlideConfig):
@field_validator("start_animation", "end_animation")
@classmethod
def index_is_posint(cls, v: int) -> int:
"""
Validate that animation indices are positive integers.
:param v: An animation index.
:return: The animation index, if valid.
"""
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
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(...)`."
)
"""
Validate that start and end animation indices satisfy 'start < end'.
:return: The config, if indices are valid.
"""
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:
@ -271,7 +349,9 @@ class SlideConfig(BaseSlideConfig):
"""Slide config to be used after rendering."""
file: FilePath
"""The file containing the animation."""
rev_file: FilePath
"""The file containing the reversed animation."""
@classmethod
def from_pre_slide_config_and_files(
@ -281,13 +361,22 @@ class SlideConfig(BaseSlideConfig):
class PresentationConfig(BaseModel): # type: ignore[misc]
"""Presentation config that contains all necessary information for a presentation."""
slides: list[SlideConfig] = Field(min_length=1)
"""The non-empty list of slide configs."""
resolution: tuple[PositiveInt, PositiveInt] = (1920, 1080)
"""The resolution of the animation files."""
background_color: Color = "black"
"""The background color of the animation files."""
@classmethod
def from_file(cls, path: Path) -> "PresentationConfig":
"""Read a presentation configuration from a file."""
"""
Read a presentation configuration from a file.
:param path: The path where the config is read from.
"""
with open(path) as f:
obj = json.load(f)
@ -304,7 +393,11 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
return cls.model_validate(obj) # type: ignore
def to_file(self, path: Path) -> None:
"""Dump the presentation configuration to a file."""
"""
Dump the presentation configuration to a file.
:param path: The path to save this config.
"""
with open(path, "w") as f:
f.write(self.model_dump_json(indent=2))
@ -315,7 +408,14 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
include_reversed: bool = True,
prefix: str = "",
) -> None:
"""Copy the files to a given directory."""
"""
Copy the files to a given directory and return the corresponding configuration.
:param folder: The folder that will contain the animation files.
:param use_cached: Whether caching should be used to avoid copies when possible.
:param include_reversed: Whether to also copy reversed animation to the folder.
:param prefix: Optional prefix added to each file name.
"""
for slide_config in self.slides:
file = slide_config.file
rev_file = slide_config.rev_file
@ -327,4 +427,6 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
shutil.copy(file, dest)
if include_reversed and (not use_cached or not rev_dest.exists()):
# TODO: if include_reversed is False, then rev_dev will likely not exist
# and this will cause an issue when decoding.
shutil.copy(rev_file, rev_dest)

View File

@ -1,4 +1,8 @@
"""Manim Slides' defaults."""
from pathlib import Path
FOLDER_PATH: Path = Path("./slides")
"""Folder where slides are stored."""
CONFIG_PATH: Path = Path(".manim-slides.toml")
"""Path to local Manim Slides config."""

View File

@ -31,7 +31,11 @@ HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
def make_logger() -> logging.Logger:
"""Make a logger similar to the one used by Manim."""
"""
Make a logger similar to the one used by Manim.
:return: The logger instance.
"""
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
rich_handler = RichHandler(
show_time=True,
@ -47,6 +51,5 @@ def make_logger() -> logging.Logger:
return logger
make_logger()
logger = logging.getLogger("manim-slides")
logger = make_logger()
"""The logger instance used across this project."""

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

@ -116,7 +116,7 @@ class ManimSlidesMagic(Magics): # type: ignore
file) will be moved relative to the video locations. Use-cases include building
documentation with Sphinx and JupyterBook. See also the
:mod:`Manim Slides directive for Sphinx
<manim_slides.docs.manim_slides_directive>`.
<manim_slides.sphinxext.manim_slides_directive>`.
Examples
--------

View File

@ -1,10 +1,12 @@
__all__ = [
"""Slides module with logic to either import ManimCE or ManimGL."""
__all__ = (
"API_NAME",
"MANIM",
"MANIMGL",
"Slide",
"ThreeDSlide",
]
)
import os
@ -14,10 +16,10 @@ import sys
class ManimApiNotFoundError(ImportError):
"""Error raised if specified manim API could be imported."""
_msg = "Could not import the specified manim API"
_msg = "Could not import the specified manim API: `{api}`."
def __init__(self) -> None:
super().__init__(self._msg)
def __init__(self, api: str) -> None:
super().__init__(self._msg.format(api=api))
API_NAMES = {
@ -26,9 +28,12 @@ API_NAMES = {
"manimlib": "manimlib",
"manimgl": "manimlib",
}
"""Allowed values for API."""
MANIM_API: str = "MANIM_API"
"""API environ variable name."""
FORCE_MANIM_API: str = "FORCE_" + MANIM_API
"""FORCE API environ variable name."""
API: str = os.environ.get(MANIM_API, "manim").lower()
@ -53,11 +58,14 @@ if MANIM:
try:
from .manim import Slide, ThreeDSlide
except ImportError as e:
raise ManimApiNotFoundError from e
raise ManimApiNotFoundError("manim") from e
elif MANIMGL:
try:
from .manimlib import Slide, ThreeDSlide
except ImportError as e:
raise ManimApiNotFoundError from e
raise ManimApiNotFoundError("manimlib") from e
else:
raise ManimApiNotFoundError
raise ValueError(
"This error should never occur. "
"Please report an issue on GitHub if you encounter it."
)

View File

@ -1,6 +1,8 @@
"""Base class for the Slide class."""
from __future__ import annotations
__all__ = ["BaseSlide"]
__all__ = ("BaseSlide",)
import platform
import shutil
@ -15,10 +17,15 @@ from typing import (
import numpy as np
from tqdm import tqdm
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
from ..defaults import FOLDER_PATH
from ..logger import logger
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
from ..core.config import (
BaseSlideConfig,
PresentationConfig,
PreSlideConfig,
SlideConfig,
)
from ..core.defaults import FOLDER_PATH
from ..core.logger import logger
from ..core.utils import concatenate_video_files, merge_basenames, reverse_video_file
from . import MANIM
if TYPE_CHECKING:
@ -36,6 +43,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
@ -303,7 +312,7 @@ class BaseSlide:
: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>`,
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::
@ -346,6 +355,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>`,
@ -469,6 +483,18 @@ 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
@ -491,7 +517,7 @@ class BaseSlide:
)
)
def _save_slides(
def _save_slides( # noqa: C901
self,
use_cache: bool = True,
flush_cache: bool = False,
@ -530,14 +556,18 @@ 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",
):
if pre_slide_config.skip_animations:
continue
slide_files = files[pre_slide_config.slides_slice]
if pre_slide_config.src:
slide_files = [pre_slide_config.src]
else:
slide_files = files[pre_slide_config.slides_slice]
try:
file = merge_basenames(slide_files)
@ -557,7 +587,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(

View File

@ -1,3 +1,5 @@
"""Manim's implementation of the Slide class."""
from pathlib import Path
from typing import Any, Optional
@ -5,30 +7,41 @@ from manim import Scene, ThreeDScene, config
from manim.renderer.opengl_renderer import OpenGLRenderer
from manim.utils.color import rgba_to_color
from ..config import BaseSlideConfig
from ..core.config import BaseSlideConfig
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:

View File

@ -1,3 +1,5 @@
"""ManimGL's implementation of the Slide class."""
from pathlib import Path
from typing import Any, ClassVar, Optional

View File

View File

@ -40,7 +40,7 @@ First, you must include the directive in the Sphinx configuration file:
extensions = [
# ...
"manim_slides.docs.manim_slides_directive",
"manim_slides.sphinxext.manim_slides_directive",
]
Its basic usage that allows processing **inline content**

View File

@ -2,6 +2,39 @@
build-backend = "hatchling.build"
requires = ["hatchling", "hatch-fancy-pypi-readme"]
[dependency-groups]
dev = [
{include-group = "docs"},
{include-group = "tests"},
"bump-my-version>=0.20.3",
"pre-commit>=3.5.0",
]
docs = [
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"pygments<2.19", # See: https://github.com/ManimCommunity/manim/issues/4104
"sphinx>=7.0.1",
"sphinxcontrib-programoutput>=0.18",
"sphinx-design>=0.6.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
]
tests = [
"importlib-metadata>=8.6.1;python_version<'3.10'",
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-missing-modules>=0.1.0",
"pytest-qt>=4.2.0",
"setuptools>=73.0.1",
]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [
@ -42,21 +75,6 @@ name = "manim-slides"
requires-python = ">=3.9"
[project.optional-dependencies]
docs = [
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"pygments<2.19", # See: https://github.com/ManimCommunity/manim/issues/4104
"sphinx>=7.0.1",
"sphinxcontrib-programoutput>=0.18",
"sphinx-design>=0.6.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
]
full = [
"manim-slides[magic,manim,sphinx-directive]",
]
@ -68,17 +86,9 @@ pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-missing-modules>=0.1.0",
"pytest-qt>=4.2.0",
]
[project.scripts]
manim-slides = "manim_slides.__main__:cli"
manim-slides = "manim_slides.cli.commands:main"
[project.urls]
Changelog = "https://github.com/jeertmans/manim-slides/releases"
@ -91,7 +101,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
allow_dirty = false
commit = true
commit_args = ""
current_version = "5.4.2"
current_version = "5.5.1"
ignore_missing_version = false
message = "chore(deps): bump version from {current_version} to {new_version}"
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
@ -198,10 +208,9 @@ filterwarnings = [
]
[tool.ruff]
extend-exclude = ["manim_slides/resources.py"]
extend-exclude = ["manim_slides/cli/resources.py"]
extend-include = ["*.ipynb"]
line-length = 88
target-version = "py39"
[tool.ruff.lint]
extend-ignore = [
@ -224,10 +233,3 @@ isort = {known-first-party = ["manim_slides", "tests"]}
[tool.ruff.lint.per-file-ignores]
"docs/source/reference/magic_example.ipynb" = ["F403", "F405"]
"tests/test_slide.py" = ["N801"]
[tool.uv]
dev-dependencies = [
"bump-my-version>=0.20.3",
"pre-commit>=3.5.0",
"setuptools>=73.0.1",
]

View File

@ -314,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):
@ -569,6 +589,51 @@ class TestSlide:
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):

298
uv.lock generated
View File

@ -1,4 +1,5 @@
version = 1
revision = 1
requires-python = ">=3.9"
resolution-markers = [
"python_full_version >= '3.10' and python_full_version < '3.13'",
@ -78,11 +79,11 @@ wheels = [
[[package]]
name = "attrs"
version = "24.3.0"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
{ url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 },
]
[[package]]
@ -224,7 +225,7 @@ wheels = [
[[package]]
name = "bump-my-version"
version = "0.29.0"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@ -236,9 +237,9 @@ dependencies = [
{ name = "tomlkit" },
{ name = "wcmatch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/277e3748f0e45758210e971ed2c54c93f1f768a5bc9c66f8cb58a604a415/bump_my_version-0.29.0.tar.gz", hash = "sha256:e4149ed63b4772f5868b3fcabb8fa5e1191b8abae6d35effd0be980d4b0f55e3", size = 1013425 }
sdist = { url = "https://files.pythonhosted.org/packages/e7/4f/57eda33958c5820b462c4c262bc18dc374dca6312bbb63f95606172200cb/bump_my_version-0.30.0.tar.gz", hash = "sha256:d53e784c73abc4bb5759e296f510bc71878e1df078eb525542ec9291b5ceb195", size = 1062228 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/59/0c78849320b0dbe17f7e67a125b6c4adcbba069c4993bf4064aea42add13/bump_my_version-0.29.0-py3-none-any.whl", hash = "sha256:6566ab25bd3eeeec109f4ac7e4464227a3ac1fd57f847d259a24800423cd9037", size = 52132 },
{ url = "https://files.pythonhosted.org/packages/41/9b/965ad61f85cbde14694516b02dcd38ec0c5cf7132fe33a30fddb4d8b0803/bump_my_version-0.30.0-py3-none-any.whl", hash = "sha256:b0d683a1cb97fbc2f46adf8eb39ff1f0bdd72866c3583fe01f9837d6f031e5e3", size = 55257 },
]
[[package]]
@ -407,7 +408,7 @@ name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
@ -823,51 +824,51 @@ wheels = [
[[package]]
name = "fonttools"
version = "4.55.4"
version = "4.55.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/8d/8912cdde6a2b4c19ced69ea5790cd17d1c095a3c0104c1c936a1de804a64/fonttools-4.55.4.tar.gz", hash = "sha256:9598af0af85073659facbe9612fcc56b071ef2f26e3819ebf9bd8c5d35f958c5", size = 3498560 }
sdist = { url = "https://files.pythonhosted.org/packages/55/55/3b1566c6186a5e58a17a19ad63195f87c6ca4039ef10ff5318a1b9fc5639/fonttools-4.55.7.tar.gz", hash = "sha256:6899e3d97225a8218f525e9754da0376e1c62953a0d57a76c5abaada51e0d140", size = 3458372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/84/da14576ce30bbed3c882bfc4de84d2e4348c65b1382688812357cb21416a/fonttools-4.55.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b332ea7b7f5f3d99f9bc5a28a23c3824ae72711abf7c4e1d62fa21699fdebe7", size = 2774346 },
{ url = "https://files.pythonhosted.org/packages/50/1d/3da7148a5552871c5dbe368de755602a0df0672e339edc133ed3e9704f2a/fonttools-4.55.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8f925909256e62152e7c3e192655dbca3ab8c3cdef7d7b436732727e80feb6", size = 2302782 },
{ url = "https://files.pythonhosted.org/packages/1b/9d/6b5be027fbfc8eab302d89608fc158b37531f3116506062e0d7183546465/fonttools-4.55.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a58af9b98e39bcd773aa352b4512be79b472830b799cb1d3cafb2b4796b71cd", size = 4584269 },
{ url = "https://files.pythonhosted.org/packages/53/6f/c5ccd4c8f90fd7f6964a1b8981e58f5cc6361acedb0a473a8dae4e1ac3c6/fonttools-4.55.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:736d750d2ab4523067d8058e5294b40b01f2eee521e0fd401bec0d5e21e80b12", size = 4626917 },
{ url = "https://files.pythonhosted.org/packages/c7/ea/53c4c75212b30d257e0865d6905eb6747ec7450b414caff742ff031eb758/fonttools-4.55.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1a9a2e7e8a9d3bfa9589db3e6c4e4c127fec252493924b2f87a67a25f9430057", size = 4581220 },
{ url = "https://files.pythonhosted.org/packages/04/4f/05d9bf9595d75ece4d65e52bd994431cff575e11f00a9444ac8b2781091e/fonttools-4.55.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87824368e994af34a95cb4279a8c711e51974b3c28d052d39d768531cc9e8e59", size = 4750636 },
{ url = "https://files.pythonhosted.org/packages/43/21/d91c8d4583e0f6ee8e08868d5ab3de44f78af8da37d47e265f5b433bd0e2/fonttools-4.55.4-cp310-cp310-win32.whl", hash = "sha256:6c36dcbfe64bce38c4d4f1d436cdc6445e969eee96eb98d98be603b5abf8c3f2", size = 2177793 },
{ url = "https://files.pythonhosted.org/packages/b8/33/c26363a57f5e766f38c84fb4e34d26d32a26398804f72e12a00c007166a1/fonttools-4.55.4-cp310-cp310-win_amd64.whl", hash = "sha256:3c53a467e5cf629acdbefc98b0f554859539fb6447bbeae4117b9ab51464ccc5", size = 2222313 },
{ url = "https://files.pythonhosted.org/packages/5c/22/cf0707f681486bf91f998c3a6a6492d806d1cf09445ce01b26a724917439/fonttools-4.55.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1605b28165c785bf26c2cbd205dc0822463e3f9f56f187049eb214dc5f4a59cb", size = 2775483 },
{ url = "https://files.pythonhosted.org/packages/09/79/11a07753a7b9ef46eaaa5e85b72558095713060aeca1393057a081fb21e3/fonttools-4.55.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d851d8b2fdb676507365d1430c3285d62c4039d0d7760d8cf2f2e5ea3aa19d73", size = 2303701 },
{ url = "https://files.pythonhosted.org/packages/93/67/173994471ddb0ff8cd45b0a2ff9fa03416152ca90bd14d1cbe1ff75fb66c/fonttools-4.55.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fb3cf1cddf08cec0338f238f950cb76fabab23a324a579e3e1f9b2ef2578329", size = 4891469 },
{ url = "https://files.pythonhosted.org/packages/16/b9/22e8be0fceaed86187ba35a1035b309e47575c68ee6ace3b66f146300f43/fonttools-4.55.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddd3208b06186ca00fbd329c0d0fed5ba209c99017cc46e2c4ea42233c2fbd00", size = 4920672 },
{ url = "https://files.pythonhosted.org/packages/cc/15/ed0f0a9d303419e7c885b3a71bfe70bb71c8f964e5b1d515056e38551c69/fonttools-4.55.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9bd98819cb585a894dda9dcb337afeb2601abf17da17de7bfbfc1bc2e4a062c7", size = 4899903 },
{ url = "https://files.pythonhosted.org/packages/b5/02/bd0da57dac3f44f37898b058659cf3beedbfd89b7d0f4b10761c9602dc1b/fonttools-4.55.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4877376c10541e8dccf14876c8476d5082338fa5d21103894894382cc245144b", size = 5067979 },
{ url = "https://files.pythonhosted.org/packages/a0/b9/c232b07c0ecaba9e522695780ca8d711b099bf87889a19a6b35a4ebfde90/fonttools-4.55.4-cp311-cp311-win32.whl", hash = "sha256:3a5e466894ec6d8a009b0eb8e02a6eb26959a318d5b7a906280c26bdadce6423", size = 2176681 },
{ url = "https://files.pythonhosted.org/packages/e3/50/2aa1cf2492e6aded4320122aed690268e97076aba1f418c0b4c68fb11a50/fonttools-4.55.4-cp311-cp311-win_amd64.whl", hash = "sha256:f595129e6f9c6402965d6295fe8c18c1945d27af0f90bdb52ff426226e647afc", size = 2223239 },
{ url = "https://files.pythonhosted.org/packages/7a/ee/c7f06da45f60c076677291470599eb9f8aae6605cbfbebbcb8ee12428e26/fonttools-4.55.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b3db72ad2d26a0e9ec694cbfb4485a8da9c095d29f66561cf935dbd19f3efcea", size = 2769913 },
{ url = "https://files.pythonhosted.org/packages/d9/a9/19aa6a9685d0bb285678850bfa22365a8376c590a7aaacc9f03d3a43beaa/fonttools-4.55.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87717808fd5953588c3ffaf512e8cab0e43c09c1da04e42ba87fa4c07d8170c7", size = 2301168 },
{ url = "https://files.pythonhosted.org/packages/00/63/88740f4333008336844aadbc9f7ef85d50e2eed779a5c33e13907a2439eb/fonttools-4.55.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f49dac626ad5bc1a0147b88e6157e3211fd440d00007f0da6c9e5f91dd5cb88e", size = 4806195 },
{ url = "https://files.pythonhosted.org/packages/7b/fa/1d103fe6e9bf174afd1c04772ca4f88e8f577f44d37b7cc8644fe5ff2620/fonttools-4.55.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d0ac8656ada8b604ae5da15d9aa075232f2181b95b51a3a2a55195222df7e7", size = 4877282 },
{ url = "https://files.pythonhosted.org/packages/b8/53/1cdd447f30598950e4bf8a2de8cd1f6573e6cb34b726cf23713a3cd8fb1e/fonttools-4.55.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:013c8b45873fa77a4ff6d25e43fecf1046cb7e8c6b32f1843117f98f3f8eac60", size = 4784688 },
{ url = "https://files.pythonhosted.org/packages/71/21/edfdcd85c1cce918d410909759a8db667f95bf3faed88141b1abfa2cefe1/fonttools-4.55.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94caad375d254a0332926512f06791f5e66c24a913ebecd6178b14f61d27c62f", size = 5012253 },
{ url = "https://files.pythonhosted.org/packages/7d/e7/7c16717b75e40f735e01d899ee152a0573e90be0e6b8fc2d47c16ba8239c/fonttools-4.55.4-cp312-cp312-win32.whl", hash = "sha256:cb3eb4bf3a0c4e431e1ccab7a33ef4f1bb32657133fff4a61dc4fcbd54b94d29", size = 2165283 },
{ url = "https://files.pythonhosted.org/packages/50/ff/85d1c1d396a3ceaabcf7cb543da56d2223d9b76429bafd6c87f4a4e880df/fonttools-4.55.4-cp312-cp312-win_amd64.whl", hash = "sha256:6914269f6ff6b20c6b5a9b19d0b752880bd8ee218d9a7d6afe9960bbf1922d98", size = 2212080 },
{ url = "https://files.pythonhosted.org/packages/09/9b/e7505e7f08c291ab28e6b5c7ae9fe92aab10f5c4b3666fc67eb59f6e454b/fonttools-4.55.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:699dd32da7258a89939567a3d71b3f8decf84da54488a2526693f0d981a76479", size = 2757277 },
{ url = "https://files.pythonhosted.org/packages/7c/15/a26ae0e5be690038cf1d62277f1007282d4d355dc30dbf0a95224fe69b0e/fonttools-4.55.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f374b18ac04fbf78f20940418aee7882be3cdcb328ded80e16c3356499f64cf", size = 2294678 },
{ url = "https://files.pythonhosted.org/packages/71/6a/20863c8ddf4dc7fd290b5ffddfc83d5918447523001b67a2dc81a0899b0d/fonttools-4.55.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b18792529ca3c24259090b6faa60bd0bdfcc4a06312e8f06d6fccab007f07193", size = 4784624 },
{ url = "https://files.pythonhosted.org/packages/94/2f/c74fa21fddd6a4c22c80f2f86820a0c960a5c0f8f46407bc9c1e1c9b9f50/fonttools-4.55.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e91d25261ebc9ff2143b95e6272f46b9f28e260b8f40feda07c80b66ff7e61d", size = 4856618 },
{ url = "https://files.pythonhosted.org/packages/00/37/1e9f1cb3b2454adac0b5fe85e940ea8d4eb174a9338e47020ec7d3cf1057/fonttools-4.55.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2695781a897395d03504fd24b60c944726b5e7b7af9ea3d922f7319d70c6fc37", size = 4765002 },
{ url = "https://files.pythonhosted.org/packages/ff/fa/36b815132a71b9df13e9c52cd198194b48eb31f9a6d041f3ec6476d8b74f/fonttools-4.55.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21de3ef5b8e5361fd01d6aef2c09dda4ede139d6b3a1f5cf621d6bea48840dfd", size = 4985906 },
{ url = "https://files.pythonhosted.org/packages/73/9a/51eb1cdc08d0883c40a3ea6d9a8ecd862bac587371bc92e0f35315688994/fonttools-4.55.4-cp313-cp313-win32.whl", hash = "sha256:0ef33fda14e39aabb892a18ed16805b0b5b4e8a801fd1815a694be9dc7f30024", size = 2163286 },
{ url = "https://files.pythonhosted.org/packages/2b/a0/6fdeb063dfb401e3efc342ef8ff6cd9e290d9895c3777dbc3850842eb1ad/fonttools-4.55.4-cp313-cp313-win_amd64.whl", hash = "sha256:e953b1614e32b6da828ae7659c8f330a593b6c4b7a4a31f8f63c01b12f0d3680", size = 2209022 },
{ url = "https://files.pythonhosted.org/packages/fc/9d/09954e57e4c06237ba0dee2565feeb31e49113320504b93e759c967bcb0e/fonttools-4.55.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dcc08dcb2be554073a72f3a8cecbc4226602ccdd0187b8f37a03a731cb931864", size = 2777045 },
{ url = "https://files.pythonhosted.org/packages/aa/68/d8416c7709a30b7b3eff0b7d51fd366508ebe2d22f1f20e7c40029ce0c48/fonttools-4.55.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7b9b414ce50f09cb692e97ff82b041ea1a21076ed9c1923206560c15ce9ad03a", size = 2304043 },
{ url = "https://files.pythonhosted.org/packages/84/21/2737e0e65e51c4f5cc07cfb2f23afee20096ed5a4d596a9f7ad9bcf3e148/fonttools-4.55.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807a1357d434ef1f4aed9bdfee7077f52dbc040b18ac98f6e417f69a48afbb5", size = 4589859 },
{ url = "https://files.pythonhosted.org/packages/a0/2e/e3d7f01f03424aa7b8cbf2b99825a28ba9df556bbb51b31464185d870682/fonttools-4.55.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a3ec7cba2e71edbc999ce3d48d34ef87cc30a36af6ff90dfc0dbc131f705fc", size = 4634830 },
{ url = "https://files.pythonhosted.org/packages/c3/6c/536159ebc77f88504124e79091c40c0663f9f048585ff7be8ab582204045/fonttools-4.55.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2964b9fe6b4a892a41a8a517bac232072a821cf2288fad1d19c6c1d19c34b0dd", size = 4583331 },
{ url = "https://files.pythonhosted.org/packages/63/d6/3c59783c4beab00d13af1bff3121f354c6c1b1fc41f22e18ac2d08f2375a/fonttools-4.55.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0b9f4f032295adeb39a8c0eefb08a7b1e90f4b7571506e5d84bb923a7afa8247", size = 4753600 },
{ url = "https://files.pythonhosted.org/packages/e7/7b/c55f7a001722bbb52b9eb8506ca1c5fe797bfc5434ad60df8945a649c572/fonttools-4.55.4-cp39-cp39-win32.whl", hash = "sha256:ee4e86280dc637a17e926cbdd32c2de148c013c3468777ae6e94c8b4449c8e93", size = 2178356 },
{ url = "https://files.pythonhosted.org/packages/c1/bf/e664a9c3d61cb1fd91cdd2d332d8039292d8d4a07d81b8675026cc2d4556/fonttools-4.55.4-cp39-cp39-win_amd64.whl", hash = "sha256:82a03920f0f524abab375dcfac8926d9596986503ee00ae435bdd71b1498f214", size = 2222836 },
{ url = "https://files.pythonhosted.org/packages/f3/5d/29b126e12df844432e188d19e74f47c2578fa5a72a122b4f41819e1e0923/fonttools-4.55.4-py3-none-any.whl", hash = "sha256:d07ad8f31038c6394a0945752458313367a0ef8125d284ee59f99e68393a3c2d", size = 1111964 },
{ url = "https://files.pythonhosted.org/packages/4f/5c/ce2fce845af9696d043ac912f15b9fac4b9002fcd9ff66b80aa513a6c43f/fonttools-4.55.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c2680a3e6e2e2d104a7ea81fb89323e1a9122c23b03d6569d0768887d0d76e69", size = 2752048 },
{ url = "https://files.pythonhosted.org/packages/07/9b/f7f9409adcf22763263c6327d2d31d538babd9ad2d63d1732c9e85d60a78/fonttools-4.55.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7831d16c95b60866772a15fdcc03772625c4bb6d858e0ad8ef3d6e48709b2ef", size = 2280495 },
{ url = "https://files.pythonhosted.org/packages/91/df/348cf4ff1becd63ed952e35e436de3f9fd3245edb74c070457b465c40a58/fonttools-4.55.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:833927d089e6585019f2c85e3f8f7d87733e3fe81cd704ebaca7afa27e2e7113", size = 4561947 },
{ url = "https://files.pythonhosted.org/packages/14/fe/48b808bdf14bb9467e4a5aaa8aa89f8aba9979d52be3f7f1962f065e933e/fonttools-4.55.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7858dc6823296a053d85b831fa8428781c6c6f06fca44582bf7b6b2ff32a9089", size = 4604618 },
{ url = "https://files.pythonhosted.org/packages/52/25/305d88761aa15a8b2761869a15db34c070e72756d166a163756c53d07b35/fonttools-4.55.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05568a66b090ed9d79aefdce2ceb180bb64fc856961deaedc29f5ad51355ce2c", size = 4558896 },
{ url = "https://files.pythonhosted.org/packages/0c/0b/c6f7877611940ab75dbe50f035d16ca5ce6d9ff2e5e65b9c76da830286ff/fonttools-4.55.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2dbc08e227fbeb716776905a7bd3c4fc62c8e37c8ef7d481acd10cb5fde12222", size = 4728347 },
{ url = "https://files.pythonhosted.org/packages/43/2c/490223b8cfaeccdef3d8819945a455aa8cc57f12f49233a3d40556b739cc/fonttools-4.55.7-cp310-cp310-win32.whl", hash = "sha256:6eb93cbba484a463b5ee83f7dd3211905f27a3871d20d90fb72de84c6c5056e3", size = 2155437 },
{ url = "https://files.pythonhosted.org/packages/37/f8/ee47526b3f03596cbed9dc7f38519cb650e7769bf9365e04bd81ff4a5302/fonttools-4.55.7-cp310-cp310-win_amd64.whl", hash = "sha256:7ff8e606f905048dc91a55a06d994b68065bf35752ae199df54a9bf30013dcaa", size = 2199898 },
{ url = "https://files.pythonhosted.org/packages/07/cb/f1dd2e31553bd03dcb4eb3af1ac6acc7fe41f26067d1bba104005ec1bb04/fonttools-4.55.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:916e1d926823b4b3b3815c59fc79f4ed670696fdd5fd9a5e690a0503eef38f79", size = 2753201 },
{ url = "https://files.pythonhosted.org/packages/21/84/f9f82093789947547b4bc86242669cde816ef4d949b23f472e47e85f125d/fonttools-4.55.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b89da448e0073408d7b2c44935f9fdae4fdc93644899f99f6102ef883ecf083c", size = 2281418 },
{ url = "https://files.pythonhosted.org/packages/46/e1/e0398d2aa7bf5400c84650fc7d85708502289bb92a40f8090e6e71cfe315/fonttools-4.55.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087ace2d06894ccdb03e6975d05da6bb9cec0c689b2a9983c059880e33a1464a", size = 4869132 },
{ url = "https://files.pythonhosted.org/packages/d4/2d/9d86cd653c758334285a5c95d1bc0a7f13b6a72fc674c6b33fef3b8e3f77/fonttools-4.55.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775ed0700ee6f781436641f18a0c61b1846a8c1aecae6da6b395c4417e2cb567", size = 4898375 },
{ url = "https://files.pythonhosted.org/packages/48/ce/f49fccb7d9f7c9c6d239434fc48546a0b37a91ba8310c7bcd5127cfeb5f6/fonttools-4.55.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9ec71d0cc0242899f87e4c230ed0b22c7b8681f288fb80e3d81c2c54c5bd2c79", size = 4877574 },
{ url = "https://files.pythonhosted.org/packages/cc/85/afe73e96a1572ba0acc86e82d52554bf69f384b431acd7a15b8c3890833b/fonttools-4.55.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d4b1c5939c0521525f45522823508e6fad21175bca978583688ea3b3736e6625", size = 5045681 },
{ url = "https://files.pythonhosted.org/packages/b8/37/dc59bc5a2f049d39b62996c806c147ae2eee5316f047a37bcf4cb9dbc4ef/fonttools-4.55.7-cp311-cp311-win32.whl", hash = "sha256:23df0f1003abaf8a435543f59583fc247e7ae1b047ee2263510e0654a5f207e0", size = 2154302 },
{ url = "https://files.pythonhosted.org/packages/86/33/281989403a57945c7871df144af3512ad3d1cd223e025b08b7f377847e6d/fonttools-4.55.7-cp311-cp311-win_amd64.whl", hash = "sha256:82163d58b43eff6e2025a25c32905fdb9042a163cc1ff82dab393e7ffc77a7d5", size = 2200818 },
{ url = "https://files.pythonhosted.org/packages/c3/f5/80ba2cef5358b0984ac1ad576daba6449f81bc44ecc0244db78210c1dc38/fonttools-4.55.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:12e81d44f762156d28b5c93a6b65d98ed73678be45b22546de8ed29736c3cb96", size = 2747631 },
{ url = "https://files.pythonhosted.org/packages/67/a3/ed291ca43193c6b4e0c69ebd01505a9a0f16c06b18d2d3f2560397a4813f/fonttools-4.55.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c26445a7be689f8b70df7d5d2e2c85ec4407bdb769902a23dd45ac44f767575d", size = 2278884 },
{ url = "https://files.pythonhosted.org/packages/04/d6/a6dbce3eb296eecba1cac8f5957863d4329d174c8e96cf6453ba82e1e11f/fonttools-4.55.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2cbafedb9462be7cf68c66b6ca1d8309842fe36b729f1b1969595f5d660e5c2", size = 4783859 },
{ url = "https://files.pythonhosted.org/packages/6b/3b/a6f66be6dc6c056bd57443d2f02b6cc123b15d67d7952f7fecfa1da64a19/fonttools-4.55.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4bde87985012adbd7559bc363d802fb335e92a07ff86a76cf02bebb0b8566d1", size = 4854977 },
{ url = "https://files.pythonhosted.org/packages/1e/a9/cc5ca0681177a47c155df732a94e7f0ef4331641dcc66250fd382505ce8f/fonttools-4.55.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:69ed0660750993150f7c4d966c0c1ffaa0385f23ccef85c2ff108062d80dd7ea", size = 4762366 },
{ url = "https://files.pythonhosted.org/packages/3f/5f/a4fb68c13e0ffffc3ad732e955a45ec1f01047fdadf8adcb48eac6a1330b/fonttools-4.55.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3098355e7a7b5ac48d5dc29684a65271187b865b85675033958b57c40364ee34", size = 4989955 },
{ url = "https://files.pythonhosted.org/packages/c9/58/325d278535405f99ca1b39d360dc255126fa9dd0e3648a046982f10f4246/fonttools-4.55.7-cp312-cp312-win32.whl", hash = "sha256:ee7aa8bb716318e3d835ef473978e22b7a39c0f1b3b08cc0b0ee1bba6f73bc1e", size = 2142771 },
{ url = "https://files.pythonhosted.org/packages/49/2f/806c4b86ccfc0a5e48bc78ea3730ca9f6207c6deeab5a57bf87e6b8596f0/fonttools-4.55.7-cp312-cp312-win_amd64.whl", hash = "sha256:e696d6e2baf4cc57ded34bb87e5d3a9e4da9732f3d9e8e2c6db0746e57a6dc0b", size = 2189608 },
{ url = "https://files.pythonhosted.org/packages/63/19/6d2f97d52a3355b713d01854f35c01d712489af6b764432697c76dc042f8/fonttools-4.55.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e10c7fb80cdfdc32244514cbea0906e9f53e3cc80d64d3389da09502fd999b55", size = 2734996 },
{ url = "https://files.pythonhosted.org/packages/48/28/1f5753fb43eb9ee3919027d1f8de52ea19698083a17ff4013e636b2d82fb/fonttools-4.55.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1101976c703ff4008a928fc3fef42caf06d035bfc4614230d7e797cbe356feb0", size = 2272393 },
{ url = "https://files.pythonhosted.org/packages/f1/2f/0a1cd55dac0ec49e42c9a03a56114ae863bc3b652e2f40743151c45b1569/fonttools-4.55.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e6dffe9cbcd163ef617fab1f81682e4d1629b7a5b9c5e598274dc2d03e88bcd", size = 4762292 },
{ url = "https://files.pythonhosted.org/packages/7d/59/30c3842759e051fc100d3fc2f9722cf9b3d1db7b1fa803c7237fd2f552fc/fonttools-4.55.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e5115a425d53be6e31cd0fe9210f62a488bccf81eb113ab5dd7f4fa88e4d81", size = 4834310 },
{ url = "https://files.pythonhosted.org/packages/a5/10/91a9e18315116bff26c4e96abcfbdb279b147fe54e55772b119dce5d8963/fonttools-4.55.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f0c45eae32d090763820756b18322a70571dada3f1cbe003debc37a9c35bc260", size = 4742671 },
{ url = "https://files.pythonhosted.org/packages/b0/6c/117aac028ad47ac375033e6113930a096db685461cbd28909d3a246b1191/fonttools-4.55.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4ebc475d43f3de2b26e0cf551eff92c24e22d1aee03dc1b33adb52fc2e6cb2", size = 4963611 },
{ url = "https://files.pythonhosted.org/packages/06/28/47518cf6f69ac07184c6d61d1ef663e22b871d549d7f2b97b08bd104859b/fonttools-4.55.7-cp313-cp313-win32.whl", hash = "sha256:371197de1283cc99f5f10eb91496520eb0e2d079312d014fd6cef9e802174c6a", size = 2140803 },
{ url = "https://files.pythonhosted.org/packages/a4/9d/df15fc73e33b30eaa3c1b1304952d7b0b6a64e0b10dc4778b65b46506d38/fonttools-4.55.7-cp313-cp313-win_amd64.whl", hash = "sha256:418ece624fbc04e199f58398ffef3eaad645baba65434871b09eb7350a3a346b", size = 2186591 },
{ url = "https://files.pythonhosted.org/packages/db/f4/23890d90cf7ea1ea67f4be7adf2055763d2b7ff32a66d1557830e245dd0c/fonttools-4.55.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ef5ee98fc320c158e4e459a5ee40d1ac3728d4ce11c3c8dfd854aa0aa5c042f", size = 2754754 },
{ url = "https://files.pythonhosted.org/packages/5e/23/a2a55b3cb4dcc678f17f9fb47249e115f1a82ab29456ba380e12a7f706ef/fonttools-4.55.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09740feed51f9ed816aebf5d82071b7fecf693ac3a7e0fc8ea433f5dc3bd92f5", size = 2281759 },
{ url = "https://files.pythonhosted.org/packages/d9/57/c4f06df5cba3f5e315014989b16a962df370d0c1b730d1e090afc30f15ea/fonttools-4.55.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d19ea483b3cd8833e9e2ee8115f3d2044d55d3743d84f9c23b48b52d7516d8", size = 4567533 },
{ url = "https://files.pythonhosted.org/packages/91/81/505923e0c9409528fb52fb361c4ea5eff5fedcc979f1c5c6a4a43e308c9a/fonttools-4.55.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c135c91d47351b84893fb6fcbb8f178eba14f7cb195850264c0675c85e4238b6", size = 4612524 },
{ url = "https://files.pythonhosted.org/packages/ca/5b/fd9a21e80b5c17f512fff838f32ed02ed07b7fedb94024b86144774ac13c/fonttools-4.55.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bee4920ebeb540849bc3555d871e2a8487e39ce8263c281f74d5b6d44d2bf1df", size = 4561004 },
{ url = "https://files.pythonhosted.org/packages/9e/cd/899ffcce6b1846fc6ef4b33c3d1ee0b87f847825ad2f0525980408f6e586/fonttools-4.55.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f3b63648600dd0081bdd6856a86d014a7f1d2d11c3c974542f866478d832e103", size = 4731298 },
{ url = "https://files.pythonhosted.org/packages/fe/85/aec7ac6f15d82ad0716eee4a3484065928168b6ffec30d850e5830d9ea50/fonttools-4.55.7-cp39-cp39-win32.whl", hash = "sha256:d4bd27f0fa5120aaa39f76de5768959bc97300e0f59a3160d466b51436a38aea", size = 2155985 },
{ url = "https://files.pythonhosted.org/packages/4b/79/f45dc7aea6f4306b29c746282d4a309a568145a4a8526f944c915626c84c/fonttools-4.55.7-cp39-cp39-win_amd64.whl", hash = "sha256:c665df9c9d99937a5bf807bace1c0c95bd13f55de8c82aaf9856b868dcbfe5d9", size = 2200420 },
{ url = "https://files.pythonhosted.org/packages/7b/6d/304a16caf63a8c193ec387b1fae1cb10072a59d34549f2eefe7e3fa9f364/fonttools-4.55.7-py3-none-any.whl", hash = "sha256:3304dfcf9ca204dd0ef691a287bd851ddd8e8250108658c0677c3fdfec853a20", size = 1089677 },
]
[[package]]
@ -1009,7 +1010,7 @@ name = "ipykernel"
version = "6.29.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "appnope", marker = "platform_system == 'Darwin'" },
{ name = "appnope", marker = "sys_platform == 'darwin'" },
{ name = "comm" },
{ name = "debugpy" },
{ name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@ -1526,7 +1527,6 @@ wheels = [
[[package]]
name = "manim-slides"
version = "5.4.1"
source = { editable = "." }
dependencies = [
{ name = "av" },
@ -1549,27 +1549,6 @@ dependencies = [
]
[package.optional-dependencies]
docs = [
{ name = "docutils" },
{ name = "furo" },
{ name = "ipykernel" },
{ name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "ipython", version = "8.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "manim" },
{ name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "myst-parser", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "nbsphinx" },
{ name = "pandoc" },
{ name = "pygments" },
{ name = "pyqt6" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "sphinx-click" },
{ name = "sphinx-copybutton" },
{ name = "sphinx-design" },
{ name = "sphinxcontrib-programoutput" },
{ name = "sphinxext-opengraph" },
]
full = [
{ name = "docutils" },
{ name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@ -1611,25 +1590,59 @@ sphinx-directive = [
{ name = "docutils" },
{ name = "manim" },
]
tests = [
{ name = "docutils" },
{ name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "ipython", version = "8.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "manim" },
{ name = "manimgl" },
{ name = "pyqt6" },
{ name = "pyside6" },
[package.dev-dependencies]
dev = [
{ name = "bump-my-version" },
{ name = "furo" },
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
{ name = "ipykernel" },
{ name = "manim-slides", extra = ["full", "magic", "manim", "manimgl", "pyqt6", "pyside6", "sphinx-directive"] },
{ name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "myst-parser", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "nbsphinx" },
{ name = "pandoc" },
{ name = "pre-commit" },
{ name = "pygments" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-env" },
{ name = "pytest-missing-modules" },
{ name = "pytest-qt" },
{ name = "setuptools" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "sphinx-click" },
{ name = "sphinx-copybutton" },
{ name = "sphinx-design" },
{ name = "sphinxcontrib-programoutput" },
{ name = "sphinxext-opengraph" },
]
[package.dev-dependencies]
dev = [
{ name = "bump-my-version" },
{ name = "pre-commit" },
docs = [
{ name = "furo" },
{ name = "ipykernel" },
{ name = "manim-slides", extra = ["magic", "manim", "pyqt6", "sphinx-directive"] },
{ name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "myst-parser", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "nbsphinx" },
{ name = "pandoc" },
{ name = "pygments" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "sphinx-click" },
{ name = "sphinx-copybutton" },
{ name = "sphinx-design" },
{ name = "sphinxcontrib-programoutput" },
{ name = "sphinxext-opengraph" },
]
tests = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
{ name = "manim-slides", extra = ["full", "manimgl", "pyqt6", "pyside6", "sphinx-directive"] },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-env" },
{ name = "pytest-missing-modules" },
{ name = "pytest-qt" },
{ name = "setuptools" },
]
@ -1639,70 +1652,89 @@ requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.12.3" },
{ name = "click", specifier = ">=8.1.3" },
{ name = "click-default-group", specifier = ">=1.2.2" },
{ name = "docutils", marker = "extra == 'docs'", specifier = ">=0.20.1" },
{ name = "docutils", marker = "extra == 'full'", specifier = ">=0.20.1" },
{ name = "docutils", marker = "extra == 'pyqt6-full'", specifier = ">=0.20.1" },
{ name = "docutils", marker = "extra == 'pyside6-full'", specifier = ">=0.20.1" },
{ name = "docutils", marker = "extra == 'sphinx-directive'", specifier = ">=0.20.1" },
{ name = "docutils", marker = "extra == 'tests'", specifier = ">=0.20.1" },
{ name = "furo", marker = "extra == 'docs'", specifier = ">=2023.5.20" },
{ name = "ipykernel", marker = "extra == 'docs'", specifier = ">=6.25.1" },
{ name = "ipython", marker = "extra == 'docs'", specifier = ">=8.12.2" },
{ name = "ipython", marker = "extra == 'full'", specifier = ">=8.12.2" },
{ name = "ipython", marker = "extra == 'magic'", specifier = ">=8.12.2" },
{ name = "ipython", marker = "extra == 'pyqt6-full'", specifier = ">=8.12.2" },
{ name = "ipython", marker = "extra == 'pyside6-full'", specifier = ">=8.12.2" },
{ name = "ipython", marker = "extra == 'tests'", specifier = ">=8.12.2" },
{ name = "jinja2", specifier = ">=3.1.2" },
{ name = "lxml", specifier = ">=4.9.2" },
{ name = "manim", marker = "extra == 'docs'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'full'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'magic'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'manim'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'pyqt6-full'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'pyside6-full'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'sphinx-directive'", specifier = ">=0.19" },
{ name = "manim", marker = "extra == 'tests'", specifier = ">=0.19" },
{ name = "manimgl", marker = "extra == 'manimgl'", specifier = ">=1.7.2" },
{ name = "manimgl", marker = "extra == 'tests'", specifier = ">=1.7.2" },
{ name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0.0" },
{ name = "nbsphinx", marker = "extra == 'docs'", specifier = ">=0.9.2" },
{ name = "numpy", specifier = ">=1.19" },
{ name = "pandoc", marker = "extra == 'docs'", specifier = ">=2.3" },
{ name = "pillow", specifier = ">=9.5.0" },
{ name = "pydantic", specifier = ">=2.0.1" },
{ name = "pydantic-extra-types", specifier = ">=2.0.0" },
{ name = "pygments", marker = "extra == 'docs'", specifier = "<2.19" },
{ name = "pyqt6", marker = "extra == 'docs'", specifier = ">=6.7.0" },
{ name = "pyqt6", marker = "extra == 'pyqt6'", specifier = ">=6.7.0" },
{ name = "pyqt6", marker = "extra == 'pyqt6-full'", specifier = ">=6.7.0" },
{ name = "pyqt6", marker = "extra == 'tests'", specifier = ">=6.7.0" },
{ name = "pyside6", marker = "extra == 'pyside6'", specifier = ">=6.6.1,!=6.8.1.1" },
{ name = "pyside6", marker = "extra == 'pyside6-full'", specifier = ">=6.6.1,!=6.8.1.1" },
{ name = "pyside6", marker = "extra == 'tests'", specifier = ">=6.6.1,!=6.8.1.1" },
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=7.4.0" },
{ name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=4.1.0" },
{ name = "pytest-env", marker = "extra == 'tests'", specifier = ">=0.8.2" },
{ name = "pytest-missing-modules", marker = "extra == 'tests'", specifier = ">=0.1.0" },
{ name = "pytest-qt", marker = "extra == 'tests'", specifier = ">=4.2.0" },
{ name = "python-pptx", specifier = ">=0.6.21" },
{ name = "qtpy", specifier = ">=2.4.1" },
{ name = "requests", specifier = ">=2.28.1" },
{ name = "rich", specifier = ">=13.3.2" },
{ name = "rtoml", specifier = ">=0.11.0" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.0.1" },
{ name = "sphinx-click", marker = "extra == 'docs'", specifier = ">=4.4.0" },
{ name = "sphinx-copybutton", marker = "extra == 'docs'", specifier = ">=0.5.1" },
{ name = "sphinx-design", marker = "extra == 'docs'", specifier = ">=0.6.1" },
{ name = "sphinxcontrib-programoutput", marker = "extra == 'docs'", specifier = ">=0.18" },
{ name = "sphinxext-opengraph", marker = "extra == 'docs'", specifier = ">=0.7.5" },
{ name = "tqdm", specifier = ">=4.64.1" },
]
provides-extras = ["full", "magic", "manim", "manimgl", "pyqt6", "pyqt6-full", "pyside6", "pyside6-full", "sphinx-directive"]
[package.metadata.requires-dev]
dev = [
{ name = "bump-my-version", specifier = ">=0.20.3" },
{ name = "furo", specifier = ">=2023.5.20" },
{ name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.6.1" },
{ name = "ipykernel", specifier = ">=6.25.1" },
{ name = "manim-slides", extras = ["full", "manimgl", "pyqt6", "pyside6", "sphinx-directive"] },
{ name = "manim-slides", extras = ["magic", "manim", "pyqt6", "sphinx-directive"] },
{ name = "myst-parser", specifier = ">=2.0.0" },
{ name = "nbsphinx", specifier = ">=0.9.2" },
{ name = "pandoc", specifier = ">=2.3" },
{ name = "pre-commit", specifier = ">=3.5.0" },
{ name = "pygments", specifier = "<2.19" },
{ name = "pytest", specifier = ">=7.4.0" },
{ name = "pytest-cov", specifier = ">=4.1.0" },
{ name = "pytest-env", specifier = ">=0.8.2" },
{ name = "pytest-missing-modules", specifier = ">=0.1.0" },
{ name = "pytest-qt", specifier = ">=4.2.0" },
{ name = "setuptools", specifier = ">=73.0.1" },
{ name = "sphinx", specifier = ">=7.0.1" },
{ name = "sphinx-click", specifier = ">=4.4.0" },
{ name = "sphinx-copybutton", specifier = ">=0.5.1" },
{ name = "sphinx-design", specifier = ">=0.6.1" },
{ name = "sphinxcontrib-programoutput", specifier = ">=0.18" },
{ name = "sphinxext-opengraph", specifier = ">=0.7.5" },
]
docs = [
{ name = "furo", specifier = ">=2023.5.20" },
{ name = "ipykernel", specifier = ">=6.25.1" },
{ name = "manim-slides", extras = ["magic", "manim", "pyqt6", "sphinx-directive"] },
{ name = "myst-parser", specifier = ">=2.0.0" },
{ name = "nbsphinx", specifier = ">=0.9.2" },
{ name = "pandoc", specifier = ">=2.3" },
{ name = "pygments", specifier = "<2.19" },
{ name = "sphinx", specifier = ">=7.0.1" },
{ name = "sphinx-click", specifier = ">=4.4.0" },
{ name = "sphinx-copybutton", specifier = ">=0.5.1" },
{ name = "sphinx-design", specifier = ">=0.6.1" },
{ name = "sphinxcontrib-programoutput", specifier = ">=0.18" },
{ name = "sphinxext-opengraph", specifier = ">=0.7.5" },
]
tests = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.6.1" },
{ name = "manim-slides", extras = ["full", "manimgl", "pyqt6", "pyside6", "sphinx-directive"] },
{ name = "pytest", specifier = ">=7.4.0" },
{ name = "pytest-cov", specifier = ">=4.1.0" },
{ name = "pytest-env", specifier = ">=0.8.2" },
{ name = "pytest-missing-modules", specifier = ">=0.1.0" },
{ name = "pytest-qt", specifier = ">=4.2.0" },
{ name = "setuptools", specifier = ">=73.0.1" },
]
@ -2061,14 +2093,14 @@ wheels = [
[[package]]
name = "mistune"
version = "3.1.0"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/6e/96fc7cb3288666c5de2c396eb0e338dc95f7a8e4920e43e38783a22d0084/mistune-3.1.0.tar.gz", hash = "sha256:dbcac2f78292b9dc066cd03b7a3a26b62d85f8159f2ea5fd28e55df79908d667", size = 94401 }
sdist = { url = "https://files.pythonhosted.org/packages/c6/1d/6b2b634e43bacc3239006e61800676aa6c41ac1836b2c57497ed27a7310b/mistune-3.1.1.tar.gz", hash = "sha256:e0740d635f515119f7d1feb6f9b192ee60f0cc649f80a8f944f905706a21654c", size = 94645 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/b3/743ffc3f59da380da504d84ccd1faf9a857a1445991ff19bf2ec754163c2/mistune-3.1.0-py3-none-any.whl", hash = "sha256:b05198cf6d671b3deba6c87ec6cf0d4eb7b72c524636eddb6dbf13823b52cee1", size = 53694 },
{ url = "https://files.pythonhosted.org/packages/c6/02/c66bdfdadbb021adb642ca4e8a5ed32ada0b4a3e4b39c5d076d19543452f/mistune-3.1.1-py3-none-any.whl", hash = "sha256:02106ac2aa4f66e769debbfa028509a275069dcffce0dfa578edd7b991ee700a", size = 53696 },
]
[[package]]
@ -2206,7 +2238,7 @@ wheels = [
[[package]]
name = "nbconvert"
version = "7.16.5"
version = "7.16.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
@ -2225,9 +2257,9 @@ dependencies = [
{ name = "pygments" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/2c/d026c0367f2be2463d4c2f5b538e28add2bc67bc13730abb7f364ae4eb8b/nbconvert-7.16.5.tar.gz", hash = "sha256:c83467bb5777fdfaac5ebbb8e864f300b277f68692ecc04d6dab72f2d8442344", size = 856367 }
sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/9e/2dcc9fe00cf55d95a8deae69384e9cea61816126e345754f6c75494d32ec/nbconvert-7.16.5-py3-none-any.whl", hash = "sha256:e12eac052d6fd03040af4166c563d76e7aeead2e9aadf5356db552a1784bd547", size = 258061 },
{ url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525 },
]
[[package]]
@ -2577,7 +2609,7 @@ name = "plumbum"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "platform_python_implementation != 'PyPy' and platform_system == 'Windows'" },
{ name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083 }
wheels = [
@ -2683,16 +2715,16 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.10.5"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 }
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 },
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
@ -2829,11 +2861,11 @@ wheels = [
[[package]]
name = "pyglet"
version = "2.1.1"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/60/bf154aba98e66bcc9d58e3d8bbea3d68a960f05b968a81693b17a76ece99/pyglet-2.1.1.tar.gz", hash = "sha256:47f49890a00e9fefc4d0ea74dc5b9d6b9be1c5455bb5746b2df118012cfa3124", size = 6515139 }
sdist = { url = "https://files.pythonhosted.org/packages/ea/4b/44b4dc01ffac9f8950d540c99159f83e2c398df2a600394feb6d2dbf7340/pyglet-2.1.2.tar.gz", hash = "sha256:6fc1fed55eb6dc80c87a7a45ac62c2a61be08cd31114b27abef8619959be7845", size = 6515611 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/2d/82c81bfc3de48a8225a594346c50eb27c819d3788fccc6c7d654f27dbbc3/pyglet-2.1.1-py3-none-any.whl", hash = "sha256:30562bfe9971a9ebeb6bb826f2c0d1649dd3d41bc8109ecc103425621f54c802", size = 961927 },
{ url = "https://files.pythonhosted.org/packages/3c/aa/edc16d04f6de8e833b4cba3068185fd2aad9e6f3a22fbc89afec36414f12/pyglet-2.1.2-py3-none-any.whl", hash = "sha256:2819fa9d66ead4b1682d1b7fa7170692d46277e1a6661b329bdba3e65288c036", size = 961981 },
]
[[package]]
@ -3393,16 +3425,16 @@ wheels = [
[[package]]
name = "referencing"
version = "0.36.1"
version = "0.36.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/32/fd98246df7a0f309b58cae68b10b6b219ef2eb66747f00dfb34422687087/referencing-0.36.1.tar.gz", hash = "sha256:ca2e6492769e3602957e9b831b94211599d2aade9477f5d44110d2530cf9aade", size = 74661 }
sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/fa/9f193ef0c9074b659009f06d7cbacc6f25b072044815bcf799b76533dbb8/referencing-0.36.1-py3-none-any.whl", hash = "sha256:363d9c65f080d0d70bc41c721dce3c7f3e77fc09f269cd5c8813da18069a6794", size = 26777 },
{ url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
]
[[package]]
@ -4148,7 +4180,7 @@ name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
wheels = [
@ -4274,11 +4306,11 @@ wheels = [
[[package]]
name = "xlsxwriter"
version = "3.2.0"
version = "3.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/c3/b36fa44a0610a0f65d2e65ba6a262cbe2554b819f1449731971f7c16ea3c/XlsxWriter-3.2.0.tar.gz", hash = "sha256:9977d0c661a72866a61f9f7a809e25ebbb0fb7036baa3b9fe74afcfca6b3cb8c", size = 198732 }
sdist = { url = "https://files.pythonhosted.org/packages/a1/08/26f69d1e9264e8107253018de9fc6b96f9219817d01c5f021e927384a8d1/xlsxwriter-3.2.2.tar.gz", hash = "sha256:befc7f92578a85fed261639fb6cde1fd51b79c5e854040847dde59d4317077dc", size = 205202 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/ea/53d1fe468e63e092cf16e2c18d16f50c29851242f9dd12d6a66e0d7f0d02/XlsxWriter-3.2.0-py3-none-any.whl", hash = "sha256:ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e", size = 159925 },
{ url = "https://files.pythonhosted.org/packages/9b/07/df054f7413bdfff5e98f75056e4ed0977d0c8716424011fac2587864d1d3/XlsxWriter-3.2.2-py3-none-any.whl", hash = "sha256:272ce861e7fa5e82a4a6ebc24511f2cb952fde3461f6c6e1a1e81d3272db1471", size = 165121 },
]
[[package]]