mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-19 11:36:37 +08:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
ef282300f1 | |||
a9ba1b4fad | |||
4cc6c2865d | |||
0483e2f861 | |||
a58ff6c388 | |||
bf512f2f73 | |||
541b175360 | |||
57ab592d36 | |||
840d1d80d9 | |||
c15cd95565 | |||
478552c528 | |||
1189f37cf3 | |||
e50271b0b2 | |||
98955bee5c | |||
2169938df7 | |||
a207248deb | |||
1c859991b8 | |||
a24915665d | |||
5cce117050 | |||
bd76fbdfd9 | |||
5b026919ea | |||
05c1a16ca3 | |||
3bd8c386b1 | |||
628c8da832 | |||
3b62e6b788 | |||
ff9aac49d7 | |||
16d9d32d33 | |||
988011ff7d | |||
3dbe12b480 | |||
6ba657c0d5 | |||
6c8ab61f9d | |||
91e6e139e3 | |||
d8acbae165 | |||
75af26e601 | |||
a8903b809d | |||
d813aaf313 | |||
d5679924b9 | |||
fb562d88ac |
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -29,7 +29,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||||
required: true
|
required: true
|
||||||
- label: Checked the [frequently qsked questions]](https://manim-slides.eertmans.be/latest/faq.html);
|
- label: Checked the [frequently asked questions](https://manim-slides.eertmans.be/latest/faq.html);
|
||||||
required: true
|
required: true
|
||||||
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
|
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
|
||||||
required: true
|
required: true
|
||||||
@ -122,7 +122,7 @@ body:
|
|||||||
This will be automatically formatted into code, so no need for backticks.
|
This will be automatically formatted into code, so no need for backticks.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
from manim import *
|
from manim import *
|
||||||
from manim_slides.slide import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
|
|
||||||
class MWE(Slide):
|
class MWE(Slide):
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -18,7 +18,7 @@ body:
|
|||||||
label: Terms
|
label: Terms
|
||||||
description: 'By submitting this issue, I have:'
|
description: 'By submitting this issue, I have:'
|
||||||
options:
|
options:
|
||||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Aenhancement+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup uv
|
- name: Setup uv
|
||||||
uses: astral-sh/setup-uv@v3
|
uses: astral-sh/setup-uv@v4
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|
||||||
@ -36,7 +36,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -67,7 +66,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
platforms: linux/arm64,linux/amd64
|
platforms: linux/arm64,linux/amd64
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
push: true
|
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/jeertmans/manim-slides:latest
|
ghcr.io/jeertmans/manim-slides:latest
|
||||||
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
|
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
|
||||||
|
18
.github/workflows/tests.yml
vendored
18
.github/workflows/tests.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-13, ubuntu-latest, windows-latest]
|
||||||
pyversion: ['3.9', '3.10', '3.11', '3.12']
|
pyversion: ['3.9', '3.10', '3.11', '3.12']
|
||||||
extras: [pyside6-full, manimgl]
|
extras: [pyside6-full, manimgl]
|
||||||
exclude:
|
exclude:
|
||||||
@ -41,7 +41,9 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
sudo apt install software-properties-common
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt-get install build-essential python${{ matrix.pyversion }}-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||||
nohup Xvfb $DISPLAY &
|
nohup Xvfb $DISPLAY &
|
||||||
|
|
||||||
- name: Install Windows dependencies
|
- name: Install Windows dependencies
|
||||||
@ -70,13 +72,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup uv
|
- name: Setup uv
|
||||||
uses: astral-sh/setup-uv@v3
|
uses: astral-sh/setup-uv@v4
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|
||||||
- name: Setup Python ${{ matrix.pyversion }}
|
|
||||||
run: uv python install ${{ matrix.pyversion }}
|
|
||||||
|
|
||||||
- name: Install manim dependencies on MacOS
|
- name: Install manim dependencies on MacOS
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
||||||
@ -96,14 +95,11 @@ jobs:
|
|||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: ssciwr/setup-mesa-dist-win@v2
|
uses: ssciwr/setup-mesa-dist-win@v2
|
||||||
|
|
||||||
- name: Install Manim Slides
|
|
||||||
run: uv sync --locked --extra tests
|
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
run: uv run pytest
|
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
|
||||||
|
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -6,7 +6,7 @@ ci:
|
|||||||
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
|
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
@ -21,13 +21,13 @@ repos:
|
|||||||
exclude: poetry.lock
|
exclude: poetry.lock
|
||||||
args: [--autofix, --trailing-commas]
|
args: [--autofix, --trailing-commas]
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.6.8
|
rev: v0.9.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.11.2
|
rev: v1.14.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies: [types-requests, types-setuptools]
|
additional_dependencies: [types-requests, types-setuptools]
|
||||||
|
@ -1 +1 @@
|
|||||||
3.11.8
|
3.11
|
||||||
|
@ -2,13 +2,10 @@ version: 2
|
|||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
tools:
|
tools:
|
||||||
python: '3.10'
|
python: '3.11'
|
||||||
apt_packages:
|
apt_packages:
|
||||||
- libpango1.0-dev
|
- libpango1.0-dev
|
||||||
- ffmpeg
|
- ffmpeg
|
||||||
jobs:
|
|
||||||
post_install:
|
|
||||||
- ipython kernel install --name "manim-slides" --user
|
|
||||||
sphinx:
|
sphinx:
|
||||||
builder: html
|
builder: html
|
||||||
configuration: docs/source/conf.py
|
configuration: docs/source/conf.py
|
||||||
|
105
CHANGELOG.md
105
CHANGELOG.md
@ -8,9 +8,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
<!-- start changelog -->
|
<!-- start changelog -->
|
||||||
|
|
||||||
(unreleased)=
|
(unreleased)=
|
||||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.7...HEAD)
|
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.1...HEAD)
|
||||||
|
|
||||||
(unreleased-added)=
|
(v5.3.1)=
|
||||||
|
## [v5.3.1](https://github.com/jeertmans/manim-slides/compare/v5.3.0...v5.3.1)
|
||||||
|
|
||||||
|
(v5.3.1-fixed)=
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed HTML template to avoid missing slides when exporting with `--one-file`.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#515](https://github.com/jeertmans/manim-slides/pull/515)
|
||||||
|
|
||||||
|
(v5.3.0)=
|
||||||
|
## [v5.3.0](https://github.com/jeertmans/manim-slides/compare/v5.2.0...v5.3.0)
|
||||||
|
|
||||||
|
(v5.3.0-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added CSS and JS inline for `manim-slides convert` if `--offline`
|
||||||
|
and `--one-file` (`-cone_file`) are used for HTML output.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
|
||||||
|
|
||||||
|
(v5.3.0-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Deprecate `-cdata_uri` in favor of `-cone_file` for `manim-slides convert`.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
|
||||||
|
- Changed template to avoid micro-stuttering with `--one-file` in HTML presentation.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#508](https://github.com/jeertmans/manim-slides/pull/508)
|
||||||
|
|
||||||
|
(v5.2.0)=
|
||||||
|
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
|
||||||
|
|
||||||
|
(v5.2.0-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The info window is now only shown in presentations when there
|
||||||
|
are multiple monitors. However, the `--show-info-window` option
|
||||||
|
was added to `manim-slides present` to force the info window.
|
||||||
|
When there are multiple monitors, the info window will no longer
|
||||||
|
be on the same monitor as the main window, unless overridden.
|
||||||
|
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
|
||||||
|
[#482](https://github.com/jeertmans/manim-slides/pull/482)
|
||||||
|
|
||||||
|
(v5.2.0-chore)=
|
||||||
|
### Chore
|
||||||
|
|
||||||
|
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies
|
||||||
|
with Manim's.
|
||||||
|
[#499](https://github.com/jeertmans/manim-slides/pull/499)
|
||||||
|
|
||||||
|
- Bump ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies,
|
||||||
|
and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`,
|
||||||
|
as the latter includes many breaking changes.
|
||||||
|
[#506](https://github.com/jeertmans/manim-slides/pull/506)
|
||||||
|
|
||||||
|
(v5.1.10)=
|
||||||
|
## [v5.1.10](https://github.com/jeertmans/manim-slides/compare/v5.1.9...v5.1.10)
|
||||||
|
|
||||||
|
(v5.1.10-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `--offline` option to `manim-slides convert` for offline
|
||||||
|
HTML presentations.
|
||||||
|
[#440](https://github.com/jeertmans/manim-slides/pull/440)
|
||||||
|
- Added documentation to config option to `manim-slides convert`
|
||||||
|
when using `--show-config`.
|
||||||
|
[#485](https://github.com/jeertmans/manim-slides/pull/485)
|
||||||
|
|
||||||
|
(v5.1.10-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
|
||||||
|
[#488](https://github.com/jeertmans/manim-slides/pull/488)
|
||||||
|
|
||||||
|
(v5.1.10-fixed)=
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed PyAV issue by pinning its version to `<14`.
|
||||||
|
A future release will contain a fix that supports both `av>=14`
|
||||||
|
and `av<14`, as their syntax differ, but the former doesn't
|
||||||
|
provide binary wheels for Python 3.9.
|
||||||
|
[#494](https://github.com/jeertmans/manim-slides/pull/494)
|
||||||
|
- Fixed blank web page when converting multiple slides into HTML.
|
||||||
|
[#497](https://github.com/jeertmans/manim-slides/pull/497)
|
||||||
|
|
||||||
|
(v5.1.9)=
|
||||||
|
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)
|
||||||
|
|
||||||
|
(v5.1.9-fixed)=
|
||||||
|
## Chore
|
||||||
|
|
||||||
|
- Fixed failing docker builds.
|
||||||
|
[#481](https://github.com/jeertmans/manim-slides/pull/481)
|
||||||
|
|
||||||
|
(v5.1.8)=
|
||||||
|
## [v5.1.8](https://github.com/jeertmans/manim-slides/compare/v5.1.7...v5.1.8)
|
||||||
|
|
||||||
|
(v5.1.8-added)=
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `manim-slides checkhealth` command to easily obtain important information
|
- Added `manim-slides checkhealth` command to easily obtain important information
|
||||||
@ -23,7 +118,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
and asset files.
|
and asset files.
|
||||||
[#470](https://github.com/jeertmans/manim-slides/pull/470)
|
[#470](https://github.com/jeertmans/manim-slides/pull/470)
|
||||||
|
|
||||||
(unreleased-chore)=
|
(v5.1.8-chore)=
|
||||||
### Chore
|
### Chore
|
||||||
|
|
||||||
- Pin `rtoml==0.9.0` on Windows platforms,
|
- Pin `rtoml==0.9.0` on Windows platforms,
|
||||||
@ -49,7 +144,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Changed project manager from Rye to uv.
|
- Changed project manager from Rye to uv.
|
||||||
[#476](https://github.com/jeertmans/manim-slides/pull/476)
|
[#476](https://github.com/jeertmans/manim-slides/pull/476)
|
||||||
|
|
||||||
(unreleased-fixed)=
|
(v5.1.8-fixed)=
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix combining assets from multiple scenes to avoid filename collision.
|
- Fix combining assets from multiple scenes to avoid filename collision.
|
||||||
@ -60,7 +155,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
|
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
|
||||||
[#465](https://github.com/jeertmans/manim-slides/pull/465)
|
[#465](https://github.com/jeertmans/manim-slides/pull/465)
|
||||||
|
|
||||||
(unreleased-removed)=
|
(v5.1.8-removed)=
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Removed `full-gl` extra, because it does not make sense to ship both
|
- Removed `full-gl` extra, because it does not make sense to ship both
|
||||||
|
@ -26,7 +26,7 @@ keywords:
|
|||||||
- PowerPoint
|
- PowerPoint
|
||||||
- Python
|
- Python
|
||||||
license: MIT
|
license: MIT
|
||||||
version: v5.1.7
|
version: v5.3.1
|
||||||
preferred-citation:
|
preferred-citation:
|
||||||
publisher:
|
publisher:
|
||||||
name: The Open Journal
|
name: The Open Journal
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
> [!IMPORTANT]
|
||||||
|
> Take the [**Manim Slides Survey**](https://forms.gle/i4scrwPQghbTQwQs5)
|
||||||
|
> to help improve this tool! Thanks in advance to all the people taking the time
|
||||||
|
> to answer this short survey! The form is open until **January 31st 2025**,
|
||||||
|
> and results will be communicated in the GitHub discussions.
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
|
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
style:
|
camera:
|
||||||
background_color: '#000000'
|
background_color: '#000000'
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
|
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
RUN apt-get update -qq \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install --no-install-recommends -y \
|
&& apt-get install --no-install-recommends -y \
|
||||||
ffmpeg \
|
|
||||||
build-essential \
|
build-essential \
|
||||||
gcc \
|
gcc \
|
||||||
cmake \
|
cmake \
|
||||||
@ -24,21 +23,22 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
|
|||||||
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
|
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
|
||||||
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
||||||
&& tlmgr install \
|
&& tlmgr install \
|
||||||
amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \
|
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||||
mathastext microtype ms physics preview ragged2e relsize rsfs \
|
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||||
|
|
||||||
# clone and build manim-slides
|
# clone and build manim-slides
|
||||||
COPY . /opt/manim-slides
|
COPY . /opt/manim-slides
|
||||||
WORKDIR /opt/manim-slides
|
WORKDIR /opt/manim-slides
|
||||||
RUN pip install --no-cache manim[jupyterlab] .[sphinx-directive]
|
|
||||||
|
RUN pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
|
||||||
|
|
||||||
ARG NB_USER=manimslidesuser
|
ARG NB_USER=manimslidesuser
|
||||||
ARG NB_UID=1000
|
ARG NB_UID=1000
|
||||||
ENV USER ${NB_USER}
|
ENV USER=${NB_USER}
|
||||||
ENV NB_UID ${NB_UID}
|
ENV NB_UID=${NB_UID}
|
||||||
ENV HOME /manim-slides
|
ENV HOME=/manim-slides
|
||||||
|
|
||||||
RUN adduser --disabled-password \
|
RUN adduser --disabled-password \
|
||||||
--gecos "Default user" \
|
--gecos "Default user" \
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
{%- for presentation_config in presentation_configs -%}
|
{%- for presentation_config in presentation_configs -%}
|
||||||
{% set outer_loop = loop %}
|
{% set outer_loop = loop %}
|
||||||
{%- for slide_config in presentation_config.slides -%}
|
{%- for slide_config in presentation_config.slides -%}
|
||||||
{%- if data_uri -%}
|
{%- if one_file -%}
|
||||||
{% set file = file_to_data_uri(slide_config.file) %}
|
{% set file = file_to_data_uri(slide_config.file) %}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{% set file = assets_dir / slide_config.file.name %}
|
{% set file = assets_dir / slide_config.file.name %}
|
||||||
@ -315,27 +315,48 @@
|
|||||||
hideCursorTime: {{ hide_cursor_time }}
|
hideCursorTime: {{ hide_cursor_time }}
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if data_uri %}
|
{% if one_file %}
|
||||||
// Fix found by @t-fritsch on GitHub
|
// Fix found by @t-fritsch and @Rapsssito on GitHub
|
||||||
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
|
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
|
||||||
function fixBase64VideoBackground(event) {
|
function setVideoBase64(video) {
|
||||||
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
|
const sources = video.querySelectorAll('source');
|
||||||
if (event.currentSlide.getAttribute('data-background-video')) {
|
// Update the source of the video
|
||||||
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
|
sources.forEach((source, i) => {
|
||||||
video = background.querySelector('video'),
|
const src = source.getAttribute('src');
|
||||||
sources = video.querySelectorAll('source');
|
if(src.match(/^data:video.*;base64$/)) {
|
||||||
|
const nextSrc = sources[i+1]?.getAttribute('src');
|
||||||
|
video.setAttribute('src', `${src},${nextSrc}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sources.forEach((source, i) => {
|
function fixBase64VideoBackground(event) {
|
||||||
const src = source.getAttribute('src');
|
// Analyze all slides backgrounds
|
||||||
if(src.match(/^data:video.*;base64$/)) {
|
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
|
||||||
const nextSrc = sources[i+1]?.getAttribute('src');
|
// Get the slide video and its sources for each background
|
||||||
video.setAttribute('src', `${src},${nextSrc}`);
|
const video = slide.querySelector('video');
|
||||||
|
if (video) {
|
||||||
|
setVideoBase64(video);
|
||||||
|
} else {
|
||||||
|
// Listen to the creation of the video element
|
||||||
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
for (const addedNode of mutation.addedNodes) {
|
||||||
|
if (addedNode.tagName === 'VIDEO') {
|
||||||
|
setVideoBase64(addedNode);
|
||||||
|
observer.disconnect(); // Stop observing once the video is handled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
observer.observe(slide, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reveal.on( 'ready', fixBase64VideoBackground );
|
}
|
||||||
Reveal.on( 'slidechanged', fixBase64VideoBackground );
|
// Setup base64 videos
|
||||||
|
Reveal.on( 'ready', fixBase64VideoBackground );
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -30,9 +30,11 @@ extensions = [
|
|||||||
# Additional
|
# Additional
|
||||||
"nbsphinx",
|
"nbsphinx",
|
||||||
"myst_parser",
|
"myst_parser",
|
||||||
|
"sphinxcontrib.programoutput",
|
||||||
"sphinxext.opengraph",
|
"sphinxext.opengraph",
|
||||||
"sphinx_click",
|
"sphinx_click",
|
||||||
"sphinx_copybutton",
|
"sphinx_copybutton",
|
||||||
|
"sphinx_design",
|
||||||
# Custom
|
# Custom
|
||||||
"manim_slides.docs.manim_slides_directive",
|
"manim_slides.docs.manim_slides_directive",
|
||||||
]
|
]
|
||||||
|
@ -29,13 +29,8 @@ ManimGL support is only guaranteed to work
|
|||||||
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
||||||
and its development is not very active.
|
and its development is not very active.
|
||||||
|
|
||||||
The typical issues are that (1) ManimGL needs an outdated NumPy version
|
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
|
||||||
and (2) ManimGL **should not** be installed from the GitHub repository,
|
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
|
||||||
at least not from the `main` branch, but from a version released to PyPI.
|
|
||||||
|
|
||||||
To solve the NumPy issue, you can safely downgrade NumPy to a version supported
|
|
||||||
by ManimGL,
|
|
||||||
while ignoring the possible *conflicting dependencies* messages from `pip` (or else).
|
|
||||||
|
|
||||||
### Presenting
|
### Presenting
|
||||||
|
|
||||||
@ -107,7 +102,7 @@ Questions related to `manim-slides convert [SCENES]... output.html`.
|
|||||||
|
|
||||||
### I moved my `.html` file and it stopped working
|
### I moved my `.html` file and it stopped working
|
||||||
|
|
||||||
If you did not specify `-cdata_uri=true` when converting,
|
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
|
||||||
then Manim Slides generated a folder containing all
|
then Manim Slides generated a folder containing all
|
||||||
the video files, in the same folder as the HTML
|
the video files, in the same folder as the HTML
|
||||||
output. As the path to video files is a relative path,
|
output. As the path to video files is a relative path,
|
||||||
|
@ -23,10 +23,10 @@ using Manim Slides presentations.
|
|||||||
|
|
||||||
Daniel publishes his presentations on *Cosmology, String Theory and related*
|
Daniel publishes his presentations on *Cosmology, String Theory and related*
|
||||||
topics on his
|
topics on his
|
||||||
[personal website](https://panopepino.github.io/Web_Page/main_page/slides.html). https://panopepino.github.io/Web_Page/main_page/slides.html
|
[personal website](https://panopepino.github.io/web_page/main_page/slides.html). https://panopepino.github.io/web_page/main_page/slides.html
|
||||||
|
|
||||||
For example, below are the slides of a seminar he gave titled
|
For example, below are the slides of a seminar he gave titled
|
||||||
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html).
|
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
|
||||||
|
|
||||||
<div style="position:relative;padding-bottom:56.25%;">
|
<div style="position:relative;padding-bottom:56.25%;">
|
||||||
<iframe
|
<iframe
|
||||||
@ -37,12 +37,12 @@ For example, below are the slides of a seminar he gave titled
|
|||||||
height="100%"
|
height="100%"
|
||||||
allowfullscreen
|
allowfullscreen
|
||||||
allow="autoplay"
|
allow="autoplay"
|
||||||
src="https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html">
|
src="https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html">
|
||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
He also shares his code on a public
|
He also shares his code on a public
|
||||||
[GitHub repository](https://github.com/PanoPepino/Manim_Theoretical).
|
[GitHub repository](https://github.com/PanoPepino/mtheoretical).
|
||||||
|
|
||||||
### Jérome Eertmans
|
### Jérome Eertmans
|
||||||
|
|
||||||
|
@ -29,19 +29,6 @@ please refer to their specific installation guidelines:
|
|||||||
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
||||||
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
||||||
|
|
||||||
:::{warning}
|
|
||||||
If you install Manim from its git repository, as suggested by ManimGL,
|
|
||||||
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
|
|
||||||
for ManimGL), otherwise it might install an unsupported version of Manim!
|
|
||||||
See [#314](https://github.com/jeertmans/manim-slides/issues/314).
|
|
||||||
|
|
||||||
Also, note that ManimGL uses outdated dependencies, and may
|
|
||||||
not work out-of-the-box. One example is NumPy: ManimGL
|
|
||||||
does not specify any restriction on this package, but
|
|
||||||
only `numpy<1.25` will work, see
|
|
||||||
[#2053](https://github.com/3b1b/manim/issues/2053).
|
|
||||||
:::
|
|
||||||
|
|
||||||
<!-- end deps -->
|
<!-- end deps -->
|
||||||
|
|
||||||
## Pip Install
|
## Pip Install
|
||||||
@ -137,12 +124,12 @@ Manim Slides is distributed under Nixpkgs >=24.05.
|
|||||||
If you are using Nix or NixOS, you can find Manim Slides under:
|
If you are using Nix or NixOS, you can find Manim Slides under:
|
||||||
|
|
||||||
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
|
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
|
||||||
includes pyqt6 (see above);
|
includes PyQt6 (see above);
|
||||||
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
||||||
module (for notebook magics), and includes IPython but not does not include
|
module (for notebook magics), and includes IPython but does not include
|
||||||
any Qt bindings.
|
any Qt binding.
|
||||||
|
|
||||||
You can try out the Manim Slides package with
|
You can try out the Manim Slides package with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix-shell -p manim ffmpeg manim-slides
|
nix-shell -p manim ffmpeg manim-slides
|
||||||
@ -160,7 +147,7 @@ nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...
|
|||||||
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
|
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
|
||||||
|
|
||||||
:::{note}
|
:::{note}
|
||||||
Nix current does not support `manimgl`.
|
Nix does not currently support `manimgl`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## When you need a Qt backend
|
## When you need a Qt backend
|
||||||
|
@ -8,3 +8,30 @@ This page contains an exhaustive list of all the commands available with `manim-
|
|||||||
:prog: manim-slides
|
:prog: manim-slides
|
||||||
:nested: full
|
:nested: full
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## All config options
|
||||||
|
|
||||||
|
Each converter has its own configuration options, which are listed below.
|
||||||
|
|
||||||
|
::::{dropdown} HTML
|
||||||
|
```{program-output} manim-slides convert --to=html --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
|
||||||
|
::::{dropdown} Zip
|
||||||
|
:::{note}
|
||||||
|
The Zip converter inherits from the HTML converter.
|
||||||
|
:::
|
||||||
|
```{program-output} manim-slides convert --to=zip --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
|
||||||
|
::::{dropdown} PDF
|
||||||
|
```{program-output} manim-slides convert --to=pdf --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
|
||||||
|
::::{dropdown} HTML
|
||||||
|
```{program-output} manim-slides convert --to=pdf --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
@ -78,9 +78,9 @@
|
|||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"kernelspec": {
|
"kernelspec": {
|
||||||
"display_name": "manim-slides",
|
"display_name": ".venv",
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"name": "manim-slides"
|
"name": "python3"
|
||||||
},
|
},
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"codemirror_mode": {
|
"codemirror_mode": {
|
||||||
@ -92,7 +92,7 @@
|
|||||||
"name": "python",
|
"name": "python",
|
||||||
"nbconvert_exporter": "python",
|
"nbconvert_exporter": "python",
|
||||||
"pygments_lexer": "ipython3",
|
"pygments_lexer": "ipython3",
|
||||||
"version": "3.10.6"
|
"version": "3.11.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
@ -137,9 +137,10 @@ and it there to preserve the original aspect ratio (16:9).
|
|||||||
|
|
||||||
### Sharing ONE HTML file
|
### Sharing ONE HTML file
|
||||||
|
|
||||||
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
|
If you set the `--one-file` flag, all animations will be data URI encoded,
|
||||||
all animations will be data URI encoded, making the HTML a self-contained
|
making the HTML a self-contained presentation file that can be shared
|
||||||
presentation file that can be shared on its own.
|
on its own. If you also set the `--offline` flag, the JS and CSS files will
|
||||||
|
be included in the HTML file as well.
|
||||||
|
|
||||||
### Over the internet
|
### Over the internet
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import json
|
|||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
from click_default_group import DefaultGroup
|
from click_default_group import DefaultGroup
|
||||||
from trogon import tui
|
|
||||||
|
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
from .checkhealth import checkhealth
|
from .checkhealth import checkhealth
|
||||||
@ -14,7 +13,6 @@ from .render import render
|
|||||||
from .wizard import init, wizard
|
from .wizard import init, wizard
|
||||||
|
|
||||||
|
|
||||||
@tui()
|
|
||||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--notify-outdated-version/--silent",
|
"--notify-outdated-version/--silent",
|
||||||
@ -73,6 +71,5 @@ cli.add_command(present)
|
|||||||
cli.add_command(render)
|
cli.add_command(render)
|
||||||
cli.add_command(wizard)
|
cli.add_command(wizard)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "5.1.7"
|
__version__ = "5.3.1"
|
||||||
|
@ -4,6 +4,8 @@ import platform
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
import warnings
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@ -15,6 +17,8 @@ from typing import Any, Callable, Optional, Union
|
|||||||
import av
|
import av
|
||||||
import click
|
import click
|
||||||
import pptx
|
import pptx
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from click import Context, Parameter
|
from click import Context, Parameter
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@ -28,7 +32,6 @@ from pydantic import (
|
|||||||
PositiveFloat,
|
PositiveFloat,
|
||||||
PositiveInt,
|
PositiveInt,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
conlist,
|
|
||||||
)
|
)
|
||||||
from pydantic_core import CoreSchema, core_schema
|
from pydantic_core import CoreSchema, core_schema
|
||||||
from pydantic_extra_types.color import Color
|
from pydantic_extra_types.color import Color
|
||||||
@ -100,9 +103,12 @@ def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
|
|||||||
|
|
||||||
|
|
||||||
class Converter(BaseModel): # type: ignore
|
class Converter(BaseModel): # type: ignore
|
||||||
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
|
presentation_configs: list[PresentationConfig]
|
||||||
assets_dir: str = "{basename}_assets"
|
assets_dir: str = Field(
|
||||||
template: Optional[Path] = None
|
"{basename}_assets",
|
||||||
|
description="Assets folder.\nThis is a template string that accepts 'dirname', 'basename', and 'ext' as variables.\nThose variables are obtained from the output filename.",
|
||||||
|
)
|
||||||
|
template: Optional[Path] = Field(None, description="Custom template file to use.")
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None:
|
||||||
"""Convert self, i.e., a list of presentations, into a given format."""
|
"""Convert self, i.e., a list of presentations, into a given format."""
|
||||||
@ -287,49 +293,153 @@ class RevealTheme(str, StrEnum):
|
|||||||
|
|
||||||
|
|
||||||
class RevealJS(Converter):
|
class RevealJS(Converter):
|
||||||
# Export option: use data-uri
|
"""
|
||||||
data_uri: bool = False
|
RevealJS options.
|
||||||
|
|
||||||
|
Please check out https://revealjs.com/config/ for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Export option:
|
||||||
|
one_file: bool = Field(
|
||||||
|
False, description="Embed all assets (e.g., animations) inside the HTML."
|
||||||
|
)
|
||||||
|
offline: bool = Field(
|
||||||
|
False, description="Download remote assets for offline presentation."
|
||||||
|
)
|
||||||
# Presentation size options from RevealJS
|
# Presentation size options from RevealJS
|
||||||
width: Union[Str, int] = Str("100%")
|
width: Union[Str, int] = Field(
|
||||||
height: Union[Str, int] = Str("100%")
|
Str("100%"), description="Width of the presentation."
|
||||||
margin: float = 0.04
|
)
|
||||||
min_scale: float = 0.2
|
height: Union[Str, int] = Field(
|
||||||
max_scale: float = 2.0
|
Str("100%"), description="Height of the presentation."
|
||||||
|
)
|
||||||
|
margin: float = Field(0.04, description="Margin to use around the content.")
|
||||||
|
min_scale: float = Field(
|
||||||
|
0.2, description="Bound for smallest possible scale to apply to content."
|
||||||
|
)
|
||||||
|
max_scale: float = Field(
|
||||||
|
2.0, description="Bound for large possible scale to apply to content."
|
||||||
|
)
|
||||||
# Configuration options from RevealJS
|
# Configuration options from RevealJS
|
||||||
controls: JsBool = JsBool.false
|
controls: JsBool = Field(
|
||||||
controls_tutorial: JsBool = JsBool.true
|
JsBool.false, description="Display presentation control arrows."
|
||||||
controls_layout: ControlsLayout = ControlsLayout.bottom_right
|
)
|
||||||
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
|
controls_tutorial: JsBool = Field(
|
||||||
progress: JsBool = JsBool.false
|
JsBool.true, description="Help the user learn the controls by providing hints."
|
||||||
slide_number: SlideNumber = SlideNumber.false
|
)
|
||||||
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
|
controls_layout: ControlsLayout = Field(
|
||||||
hash_one_based_index: JsBool = JsBool.false
|
ControlsLayout.bottom_right, description="Determine where controls appear."
|
||||||
hash: JsBool = JsBool.false
|
)
|
||||||
respond_to_hash_changes: JsBool = JsBool.false
|
controls_back_arrows: ControlsBackArrows = Field(
|
||||||
history: JsBool = JsBool.false
|
ControlsBackArrows.faded,
|
||||||
keyboard: JsBool = JsBool.true
|
description="Visibility rule for backwards navigation arrows.",
|
||||||
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
|
)
|
||||||
disable_layout: JsBool = JsBool.false
|
progress: JsBool = Field(
|
||||||
overview: JsBool = JsBool.true
|
JsBool.false, description="Display a presentation progress bar."
|
||||||
center: JsBool = JsBool.true
|
)
|
||||||
touch: JsBool = JsBool.true
|
slide_number: SlideNumber = Field(
|
||||||
loop: JsBool = JsBool.false
|
SlideNumber.false, description="Display the page number of the current slide."
|
||||||
rtl: JsBool = JsBool.false
|
)
|
||||||
navigation_mode: NavigationMode = NavigationMode.default
|
show_slide_number: Union[ShowSlideNumber, Function] = Field(
|
||||||
shuffle: JsBool = JsBool.false
|
ShowSlideNumber.all,
|
||||||
fragments: JsBool = JsBool.true
|
description="Can be used to limit the contexts in which the slide number appears.",
|
||||||
fragment_in_url: JsBool = JsBool.true
|
)
|
||||||
embedded: JsBool = JsBool.false
|
hash_one_based_index: JsBool = Field(
|
||||||
help: JsBool = JsBool.true
|
JsBool.false,
|
||||||
pause: JsBool = JsBool.true
|
description="Use 1 based indexing for # links to match slide number (default is zero based).",
|
||||||
show_notes: JsBool = JsBool.false
|
)
|
||||||
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
|
hash: JsBool = Field(
|
||||||
preload_iframes: PreloadIframes = PreloadIframes.null
|
JsBool.false,
|
||||||
auto_animate: JsBool = JsBool.true
|
description="Add the current slide number to the URL hash so that reloading the page/copying the URL will return you to the same slide.",
|
||||||
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
|
)
|
||||||
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
|
respond_to_hash_changes: JsBool = Field(
|
||||||
auto_animate_duration: float = 1.0
|
JsBool.false,
|
||||||
auto_animate_unmatched: JsBool = JsBool.true
|
description="Flags if we should monitor the hash and change slides accordingly.",
|
||||||
|
)
|
||||||
|
jump_to_slide: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Enable support for jump-to-slide navigation shortcuts.",
|
||||||
|
)
|
||||||
|
history: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Push each slide change to the browser history. Implies `hash: true`.",
|
||||||
|
)
|
||||||
|
keyboard: JsBool = Field(
|
||||||
|
JsBool.true, description="Enable keyboard shortcuts for navigation."
|
||||||
|
)
|
||||||
|
keyboard_condition: Union[KeyboardCondition, Function] = Field(
|
||||||
|
KeyboardCondition.null,
|
||||||
|
description="Optional function that blocks keyboard events when retuning false.",
|
||||||
|
)
|
||||||
|
disable_layout: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Disable the default reveal.js slide layout (scaling and centering) so that you can use custom CSS layout.",
|
||||||
|
)
|
||||||
|
overview: JsBool = Field(JsBool.true, description="Enable the slide overview mode.")
|
||||||
|
center: JsBool = Field(JsBool.true, description="Vertical centering of slides.")
|
||||||
|
touch: JsBool = Field(
|
||||||
|
JsBool.true, description="Enable touch navigation on devices with touch input."
|
||||||
|
)
|
||||||
|
loop: JsBool = Field(JsBool.false, description="Loop the presentation.")
|
||||||
|
rtl: JsBool = Field(
|
||||||
|
JsBool.false, description="Change the presentation direction to be RTL."
|
||||||
|
)
|
||||||
|
navigation_mode: NavigationMode = Field(
|
||||||
|
NavigationMode.default,
|
||||||
|
description="Change the behavior of our navigation directions.",
|
||||||
|
)
|
||||||
|
shuffle: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Randomize the order of slides each time the presentation loads.",
|
||||||
|
)
|
||||||
|
fragments: JsBool = Field(
|
||||||
|
JsBool.true, description="Turns fragment on and off globally."
|
||||||
|
)
|
||||||
|
fragment_in_url: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Flag whether to include the current fragment in the URL, so that reloading brings you to the same fragment position.",
|
||||||
|
)
|
||||||
|
embedded: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Flag if the presentation is running in an embedded mode, i.e. contained within a limited portion of the screen.",
|
||||||
|
)
|
||||||
|
help: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Flag if we should show a help overlay when the question-mark key is pressed.",
|
||||||
|
)
|
||||||
|
pause: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Flag if it should be possible to pause the presentation (blackout).",
|
||||||
|
)
|
||||||
|
show_notes: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Flag if speaker notes should be visible to all viewers.",
|
||||||
|
)
|
||||||
|
auto_play_media: AutoPlayMedia = Field(
|
||||||
|
AutoPlayMedia.null,
|
||||||
|
description="Global override for autolaying embedded media (video/audio/iframe).",
|
||||||
|
)
|
||||||
|
preload_iframes: PreloadIframes = Field(
|
||||||
|
PreloadIframes.null,
|
||||||
|
description="Global override for preloading lazy-loaded iframes.",
|
||||||
|
)
|
||||||
|
auto_animate: JsBool = Field(
|
||||||
|
JsBool.true, description="Can be used to globally disable auto-animation."
|
||||||
|
)
|
||||||
|
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = Field(
|
||||||
|
AutoAnimateMatcher.null,
|
||||||
|
description="Optionally provide a custom element matcher that will be used to dictate which elements we can animate between.",
|
||||||
|
)
|
||||||
|
auto_animate_easing: AutoAnimateEasing = Field(
|
||||||
|
AutoAnimateEasing.ease,
|
||||||
|
description="Default settings for our auto-animate transitions, can be overridden per-slide or per-element via data arguments.",
|
||||||
|
)
|
||||||
|
auto_animate_duration: float = Field(
|
||||||
|
1.0, description="See 'auto_animate_easing' documentation."
|
||||||
|
)
|
||||||
|
auto_animate_unmatched: JsBool = Field(
|
||||||
|
JsBool.true, description="See 'auto_animate_easing' documentation."
|
||||||
|
)
|
||||||
auto_animate_styles: list[str] = Field(
|
auto_animate_styles: list[str] = Field(
|
||||||
default_factory=lambda: [
|
default_factory=lambda: [
|
||||||
"opacity",
|
"opacity",
|
||||||
@ -344,34 +454,88 @@ class RevealJS(Converter):
|
|||||||
"border-radius",
|
"border-radius",
|
||||||
"outline",
|
"outline",
|
||||||
"outline-offset",
|
"outline-offset",
|
||||||
]
|
],
|
||||||
|
description="CSS properties that can be auto-animated.",
|
||||||
|
)
|
||||||
|
auto_slide: AutoSlide = Field(
|
||||||
|
0, description="Control automatic progression to the next slide."
|
||||||
|
)
|
||||||
|
auto_slide_stoppable: JsBool = Field(
|
||||||
|
JsBool.true, description="Stop auto-sliding after user input."
|
||||||
|
)
|
||||||
|
auto_slide_method: Union[AutoSlideMethod, Function] = Field(
|
||||||
|
AutoSlideMethod.null,
|
||||||
|
description="Use this method for navigation when auto-sliding (defaults to navigateNext).",
|
||||||
|
)
|
||||||
|
default_timing: Union[JsNull, int] = Field(
|
||||||
|
JsNull.null,
|
||||||
|
description="Specify the average time in seconds that you think you will spend presenting each slide.",
|
||||||
|
)
|
||||||
|
mouse_wheel: JsBool = Field(
|
||||||
|
JsBool.false, description="Enable slide navigation via mouse wheel."
|
||||||
|
)
|
||||||
|
preview_links: JsBool = Field(
|
||||||
|
JsBool.false, description="Open links in an iframe preview overlay."
|
||||||
|
)
|
||||||
|
post_message: JsBool = Field(
|
||||||
|
JsBool.true, description="Expose the reveal.js API through window.postMessage."
|
||||||
|
)
|
||||||
|
post_message_events: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Dispatch all reveal.js events to the parent window through postMessage.",
|
||||||
|
)
|
||||||
|
focus_body_on_page_visibility_change: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Focus body when page changes visibility to ensure keyboard shortcuts work.",
|
||||||
|
)
|
||||||
|
transition: Transition = Field(Transition.none, description="Transition style.")
|
||||||
|
transition_speed: TransitionSpeed = Field(
|
||||||
|
TransitionSpeed.default, description="Transition speed."
|
||||||
|
)
|
||||||
|
background_size: BackgroundSize = Field(
|
||||||
|
BackgroundSize.contain, description="Background size attribute for each video."
|
||||||
|
) # Not in RevealJS
|
||||||
|
background_transition: BackgroundTransition = Field(
|
||||||
|
BackgroundTransition.none,
|
||||||
|
description="Transition style for full page slide backgrounds.",
|
||||||
|
)
|
||||||
|
pdf_max_pages_per_slide: Union[int, str] = Field(
|
||||||
|
"Number.POSITIVE_INFINITY",
|
||||||
|
description="The maximum number of pages a single slide can expand onto when printing to PDF, unlimited by default.",
|
||||||
|
)
|
||||||
|
pdf_separate_fragments: JsBool = Field(
|
||||||
|
JsBool.true, description="Print each fragment on a separate slide."
|
||||||
|
)
|
||||||
|
pdf_page_height_offset: int = Field(
|
||||||
|
-1,
|
||||||
|
description="Offset used to reduce the height of content within exported PDF pages.",
|
||||||
|
)
|
||||||
|
view_distance: int = Field(
|
||||||
|
3, description="Number of slides away from the current that are visible."
|
||||||
|
)
|
||||||
|
mobile_view_distance: int = Field(
|
||||||
|
2,
|
||||||
|
description="Number of slides away from the current that are visible on mobile devices.",
|
||||||
|
)
|
||||||
|
display: Display = Field(
|
||||||
|
Display.block, description="The display mode that will be used to show slides."
|
||||||
|
)
|
||||||
|
hide_inactive_cursor: JsBool = Field(
|
||||||
|
JsBool.true, description="Hide cursor if inactive."
|
||||||
|
)
|
||||||
|
hide_cursor_time: int = Field(
|
||||||
|
5000, description="Time before the cursor is hidden (in ms)."
|
||||||
)
|
)
|
||||||
auto_slide: AutoSlide = 0
|
|
||||||
auto_slide_stoppable: JsBool = JsBool.true
|
|
||||||
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
|
|
||||||
default_timing: Union[JsNull, int] = JsNull.null
|
|
||||||
mouse_wheel: JsBool = JsBool.false
|
|
||||||
preview_links: JsBool = JsBool.false
|
|
||||||
post_message: JsBool = JsBool.true
|
|
||||||
post_message_events: JsBool = JsBool.false
|
|
||||||
focus_body_on_page_visibility_change: JsBool = JsBool.true
|
|
||||||
transition: Transition = Transition.none
|
|
||||||
transition_speed: TransitionSpeed = TransitionSpeed.default
|
|
||||||
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
|
|
||||||
background_transition: BackgroundTransition = BackgroundTransition.none
|
|
||||||
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
|
|
||||||
pdf_separate_fragments: JsBool = JsBool.true
|
|
||||||
pdf_page_height_offset: int = -1
|
|
||||||
view_distance: int = 3
|
|
||||||
mobile_view_distance: int = 2
|
|
||||||
display: Display = Display.block
|
|
||||||
hide_inactive_cursor: JsBool = JsBool.true
|
|
||||||
hide_cursor_time: int = 5000
|
|
||||||
# Appearance options from RevealJS
|
# Appearance options from RevealJS
|
||||||
background_color: Color = "black"
|
background_color: Color = Field(
|
||||||
reveal_version: str = "5.1.0"
|
"black",
|
||||||
reveal_theme: RevealTheme = RevealTheme.black
|
description="Background color used in slides, not relevant if videos fill the whole area.",
|
||||||
title: str = "Manim Slides"
|
)
|
||||||
|
reveal_version: str = Field("5.1.0", description="RevealJS version.")
|
||||||
|
reveal_theme: RevealTheme = Field(
|
||||||
|
RevealTheme.black, description="RevealJS version."
|
||||||
|
)
|
||||||
|
title: str = Field("Manim Slides", description="Presentation title.")
|
||||||
# Pydantic options
|
# Pydantic options
|
||||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||||
|
|
||||||
@ -385,27 +549,24 @@ class RevealJS(Converter):
|
|||||||
def open(self, file: Path) -> None:
|
def open(self, file: Path) -> None:
|
||||||
webbrowser.open(file.absolute().as_uri())
|
webbrowser.open(file.absolute().as_uri())
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None: # noqa: C901
|
||||||
"""
|
"""
|
||||||
Convert this configuration into a RevealJS HTML presentation, saved to
|
Convert this configuration into a RevealJS HTML presentation, saved to
|
||||||
DEST.
|
DEST.
|
||||||
"""
|
"""
|
||||||
if self.data_uri:
|
dirname = dest.parent
|
||||||
assets_dir = Path("") # Actually we won't care.
|
basename = dest.stem
|
||||||
else:
|
ext = dest.suffix
|
||||||
dirname = dest.parent
|
|
||||||
basename = dest.stem
|
|
||||||
ext = dest.suffix
|
|
||||||
|
|
||||||
assets_dir = Path(
|
assets_dir = Path(
|
||||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||||
)
|
)
|
||||||
full_assets_dir = dirname / assets_dir
|
full_assets_dir = dirname / assets_dir
|
||||||
|
|
||||||
|
if not self.one_file or self.offline:
|
||||||
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
||||||
|
|
||||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
if not self.one_file:
|
||||||
|
|
||||||
num_presentation_configs = len(self.presentation_configs)
|
num_presentation_configs = len(self.presentation_configs)
|
||||||
|
|
||||||
if num_presentation_configs > 1:
|
if num_presentation_configs > 1:
|
||||||
@ -424,6 +585,7 @@ class RevealJS(Converter):
|
|||||||
def prefix(i: int) -> str:
|
def prefix(i: int) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
for i, presentation_config in enumerate(self.presentation_configs):
|
for i, presentation_config in enumerate(self.presentation_configs):
|
||||||
presentation_config.copy_to(
|
presentation_config.copy_to(
|
||||||
full_assets_dir, include_reversed=False, prefix=prefix(i)
|
full_assets_dir, include_reversed=False, prefix=prefix(i)
|
||||||
@ -435,7 +597,9 @@ class RevealJS(Converter):
|
|||||||
revealjs_template = Template(self.load_template())
|
revealjs_template = Template(self.load_template())
|
||||||
|
|
||||||
options = self.model_dump()
|
options = self.model_dump()
|
||||||
options["assets_dir"] = assets_dir
|
|
||||||
|
if assets_dir is not None:
|
||||||
|
options["assets_dir"] = assets_dir
|
||||||
|
|
||||||
has_notes = any(
|
has_notes = any(
|
||||||
slide_config.notes != ""
|
slide_config.notes != ""
|
||||||
@ -448,9 +612,50 @@ class RevealJS(Converter):
|
|||||||
get_duration_ms=get_duration_ms,
|
get_duration_ms=get_duration_ms,
|
||||||
has_notes=has_notes,
|
has_notes=has_notes,
|
||||||
env=os.environ,
|
env=os.environ,
|
||||||
|
prefix=prefix if not self.one_file else None,
|
||||||
**options,
|
**options,
|
||||||
)
|
)
|
||||||
|
# If not offline, write the content to the file
|
||||||
|
if not self.offline:
|
||||||
|
f.write(content)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If offline, download remote assets and store them in the assets folder
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
for tag, inner in [("link", "href"), ("script", "src")]:
|
||||||
|
for item in soup.find_all(tag):
|
||||||
|
if item.has_attr(inner) and (link := item[inner]).startswith(
|
||||||
|
"http"
|
||||||
|
):
|
||||||
|
asset_name = link.rsplit("/", 1)[1]
|
||||||
|
asset = session.get(link)
|
||||||
|
if self.one_file:
|
||||||
|
# If it is a CSS file, inline it
|
||||||
|
if tag == "link" and "stylesheet" in item["rel"]:
|
||||||
|
item.decompose()
|
||||||
|
style = soup.new_tag("style")
|
||||||
|
style.string = asset.text
|
||||||
|
soup.head.append(style)
|
||||||
|
# If it is a JS file, inline it
|
||||||
|
elif tag == "script":
|
||||||
|
item.decompose()
|
||||||
|
script = soup.new_tag("script")
|
||||||
|
script.string = asset.text
|
||||||
|
soup.head.append(script)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unable to inline {tag} asset: {link}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(full_assets_dir / asset_name, "wb") as asset_file:
|
||||||
|
asset_file.write(asset.content)
|
||||||
|
|
||||||
|
item[inner] = str(assets_dir / asset_name)
|
||||||
|
|
||||||
|
content = str(soup)
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
@ -476,10 +681,18 @@ class FrameIndex(str, Enum):
|
|||||||
first = "first"
|
first = "first"
|
||||||
last = "last"
|
last = "last"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class PDF(Converter):
|
class PDF(Converter):
|
||||||
frame_index: FrameIndex = FrameIndex.last
|
frame_index: FrameIndex = Field(
|
||||||
resolution: PositiveFloat = 100.0
|
FrameIndex.last,
|
||||||
|
description="What frame (first or last) is used to represent each slide.",
|
||||||
|
)
|
||||||
|
resolution: PositiveFloat = Field(
|
||||||
|
100.0, description="Image resolution use for saving frames."
|
||||||
|
)
|
||||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None:
|
||||||
@ -508,12 +721,28 @@ class PDF(Converter):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPoint(Converter):
|
class PowerPoint(Converter):
|
||||||
left: PositiveInt = 0
|
left: PositiveInt = Field(
|
||||||
top: PositiveInt = 0
|
0, description="Horizontal offset where the video is placed from left border."
|
||||||
width: PositiveInt = 1280
|
)
|
||||||
height: PositiveInt = 720
|
top: PositiveInt = Field(
|
||||||
auto_play_media: bool = True
|
0, description="Vertical offset where the video is placed from top border."
|
||||||
poster_frame_image: Optional[FilePath] = None
|
)
|
||||||
|
width: PositiveInt = Field(
|
||||||
|
1280,
|
||||||
|
description="Width of the slides.\nThis should match the resolution of the presentation.",
|
||||||
|
)
|
||||||
|
height: PositiveInt = Field(
|
||||||
|
720,
|
||||||
|
description="Height of the slides.\nThis should match the resolution of the presentation.",
|
||||||
|
)
|
||||||
|
auto_play_media: bool = Field(
|
||||||
|
True, description="Automatically play animations when changing slide."
|
||||||
|
)
|
||||||
|
poster_frame_image: Optional[FilePath] = Field(
|
||||||
|
None,
|
||||||
|
description="Optional image to use when animations are not playing.\n"
|
||||||
|
"By default, the first frame of each animation is used.\nThis is important to avoid blinking effects between slides.",
|
||||||
|
)
|
||||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None:
|
||||||
@ -590,22 +819,39 @@ class PowerPoint(Converter):
|
|||||||
|
|
||||||
|
|
||||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
"""Wrap a function to add a `--show-config` option."""
|
"""Wrap a function to add a '--show-config' option."""
|
||||||
|
|
||||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
def callback(ctx: Context, _param: Parameter, value: bool) -> None:
|
||||||
if not value or ctx.resilient_parsing:
|
if not value or ctx.resilient_parsing:
|
||||||
return
|
return
|
||||||
|
|
||||||
to = ctx.params.get("to", "html")
|
if "to" in ctx.params:
|
||||||
|
to = ctx.params["to"]
|
||||||
|
cls = Converter.from_string(to)
|
||||||
|
elif "dest" in ctx.params:
|
||||||
|
dest = Path(ctx.params["dest"])
|
||||||
|
fmt = dest.suffix[1:].lower()
|
||||||
|
try:
|
||||||
|
cls = Converter.from_string(fmt)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||||
|
)
|
||||||
|
cls = RevealJS
|
||||||
|
else:
|
||||||
|
cls = RevealJS
|
||||||
|
|
||||||
converter = Converter.from_string(to)
|
if doc := getattr(cls, "__doc__", ""):
|
||||||
|
click.echo(textwrap.dedent(doc))
|
||||||
|
|
||||||
for key, field in converter.model_fields.items():
|
for key, field in cls.model_fields.items():
|
||||||
if field.is_required():
|
if field.is_required():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
default = field.get_default(call_default_factory=True)
|
default = field.get_default(call_default_factory=True)
|
||||||
click.echo(f"{key}: {default}")
|
click.echo(click.style(key, bold=True) + f": {default}")
|
||||||
|
if description := field.description:
|
||||||
|
click.secho(textwrap.indent(description, prefix="# "), dim=True)
|
||||||
|
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
|
||||||
@ -621,18 +867,31 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
|
|
||||||
|
|
||||||
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
"""Wrap a function to add a `--show-template` option."""
|
"""Wrap a function to add a '--show-template' option."""
|
||||||
|
|
||||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
if not value or ctx.resilient_parsing:
|
if not value or ctx.resilient_parsing:
|
||||||
return
|
return
|
||||||
|
|
||||||
to = ctx.params.get("to", "html")
|
if "to" in ctx.params:
|
||||||
|
to = ctx.params["to"]
|
||||||
|
cls = Converter.from_string(to)
|
||||||
|
elif "dest" in ctx.params:
|
||||||
|
dest = Path(ctx.params["dest"])
|
||||||
|
fmt = dest.suffix[1:].lower()
|
||||||
|
try:
|
||||||
|
cls = Converter.from_string(fmt)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||||
|
)
|
||||||
|
cls = RevealJS
|
||||||
|
else:
|
||||||
|
cls = RevealJS
|
||||||
|
|
||||||
template = ctx.params.get("template", None)
|
template = ctx.params.get("template", None)
|
||||||
|
|
||||||
converter = Converter.from_string(to)(
|
converter = cls(presentation_configs=[], template=template)
|
||||||
presentation_configs=[PresentationConfig()], template=template
|
|
||||||
)
|
|
||||||
click.echo(converter.load_template())
|
click.echo(converter.load_template())
|
||||||
|
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
@ -666,7 +925,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Open the newly created file using the appropriate application.",
|
help="Open the newly created file using the appropriate application.",
|
||||||
)
|
)
|
||||||
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
@ -674,7 +932,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
multiple=True,
|
multiple=True,
|
||||||
callback=validate_config_option,
|
callback=validate_config_option,
|
||||||
help="Configuration options passed to the converter. "
|
help="Configuration options passed to the converter. "
|
||||||
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
|
"E.g., pass '-cslide_number=true' to display slide numbers.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--use-template",
|
"--use-template",
|
||||||
@ -682,7 +940,19 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||||
help="Use the template given by FILE instead of default one. "
|
help="Use the template given by FILE instead of default one. "
|
||||||
"To echo the default template, use ``--show-template``.",
|
"To echo the default template, use '--show-template'.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--one-file",
|
||||||
|
is_flag=True,
|
||||||
|
help="Embed all local assets (e.g., video files) in the output file. "
|
||||||
|
"The is a convenient alias to '-cone_file=true'.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--offline",
|
||||||
|
is_flag=True,
|
||||||
|
help="Download any remote content and store it in the assets folder. "
|
||||||
|
"The is a convenient alias to '-coffline=true'.",
|
||||||
)
|
)
|
||||||
@show_template_option
|
@show_template_option
|
||||||
@show_config_options
|
@show_config_options
|
||||||
@ -693,9 +963,10 @@ def convert(
|
|||||||
dest: Path,
|
dest: Path,
|
||||||
to: str,
|
to: str,
|
||||||
open_result: bool,
|
open_result: bool,
|
||||||
force: bool,
|
|
||||||
config_options: dict[str, str],
|
config_options: dict[str, str],
|
||||||
template: Optional[Path],
|
template: Optional[Path],
|
||||||
|
offline: bool,
|
||||||
|
one_file: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||||
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||||
@ -713,6 +984,35 @@ def convert(
|
|||||||
else:
|
else:
|
||||||
cls = Converter.from_string(to)
|
cls = Converter.from_string(to)
|
||||||
|
|
||||||
|
if (
|
||||||
|
one_file
|
||||||
|
and issubclass(cls, (RevealJS, HtmlZip))
|
||||||
|
and "one_file" not in config_options
|
||||||
|
):
|
||||||
|
config_options["one_file"] = "true"
|
||||||
|
|
||||||
|
# Change data_uri to one_file and print a warning if present
|
||||||
|
if "data_uri" in config_options:
|
||||||
|
warnings.warn(
|
||||||
|
"The 'data_uri' configuration option is deprecated and will be "
|
||||||
|
"removed in the next major version. "
|
||||||
|
"Use 'one_file' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
config_options["one_file"] = (
|
||||||
|
config_options["one_file"]
|
||||||
|
if "one_file" in config_options
|
||||||
|
else config_options.pop("data_uri")
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
offline
|
||||||
|
and issubclass(cls, (RevealJS, HtmlZip))
|
||||||
|
and "offline" not in config_options
|
||||||
|
):
|
||||||
|
config_options["offline"] = "true"
|
||||||
|
|
||||||
converter = cls(
|
converter = cls(
|
||||||
presentation_configs=presentation_configs,
|
presentation_configs=presentation_configs,
|
||||||
template=template,
|
template=template,
|
||||||
|
@ -339,7 +339,7 @@ class ManimSlidesDirective(Directive):
|
|||||||
ref_block = ""
|
ref_block = ""
|
||||||
|
|
||||||
if "quality" in self.options:
|
if "quality" in self.options:
|
||||||
quality = f'{self.options["quality"]}_quality'
|
quality = f"{self.options['quality']}_quality"
|
||||||
else:
|
else:
|
||||||
quality = "example_quality"
|
quality = "example_quality"
|
||||||
frame_rate = QUALITIES[quality]["frame_rate"]
|
frame_rate = QUALITIES[quality]["frame_rate"]
|
||||||
@ -483,7 +483,7 @@ def _log_rendering_times(*args):
|
|||||||
)
|
)
|
||||||
for row in group:
|
for row in group:
|
||||||
print( # noqa: T201
|
print( # noqa: T201
|
||||||
f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}"
|
f"{' ' * (max_file_length)} {row[2].rjust(7)}s {row[1]}"
|
||||||
)
|
)
|
||||||
print("") # noqa: T201
|
print("") # noqa: T201
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class ManimSlidesMagic(Magics): # type: ignore
|
|||||||
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
|
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
|
||||||
could look as follows::
|
could look as follows::
|
||||||
|
|
||||||
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
|
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true one_file=true
|
||||||
|
|
||||||
class MySlide(Slide):
|
class MySlide(Slide):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
@ -222,17 +222,29 @@ class ManimSlidesMagic(Magics): # type: ignore
|
|||||||
|
|
||||||
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
|
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
|
||||||
|
|
||||||
if embed: # Embedding implies data-uri
|
# If data_uri is set, raise a warning
|
||||||
kwargs["data_uri"] = "true"
|
if "data_uri" in kwargs:
|
||||||
|
logger.warning(
|
||||||
|
"'data_uri' configuration option is deprecated and will be removed in a future release. "
|
||||||
|
"Please use 'one_file' instead."
|
||||||
|
)
|
||||||
|
kwargs["one_file"] = (
|
||||||
|
kwargs["one_file"]
|
||||||
|
if "one_file" in kwargs
|
||||||
|
else kwargs.pop("data_uri")
|
||||||
|
)
|
||||||
|
|
||||||
|
if embed: # Embedding implies one_file
|
||||||
|
kwargs["one_file"] = "true"
|
||||||
|
|
||||||
# TODO: FIXME
|
# TODO: FIXME
|
||||||
# Seems like files are blocked so date-uri is the only working option...
|
# Seems like files are blocked so one_file is the only working option...
|
||||||
if kwargs.get("data_uri", "false").lower().strip() == "false":
|
if kwargs.get("one_file", "false").lower().strip() == "false":
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"data_uri option is currently automatically enabled, "
|
"one_file option is currently automatically enabled, "
|
||||||
"because using local video files does not seem to work properly."
|
"because using local video files does not seem to work properly."
|
||||||
)
|
)
|
||||||
kwargs["data_uri"] = "true"
|
kwargs["one_file"] = "true"
|
||||||
|
|
||||||
presentation_configs = get_scenes_presentation_config(
|
presentation_configs = get_scenes_presentation_config(
|
||||||
[clsname], Path("./slides")
|
[clsname], Path("./slides")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from click import Context, Parameter
|
from click import Context, Parameter
|
||||||
@ -222,8 +222,14 @@ def start_at_callback(
|
|||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--hide-info-window",
|
"--hide-info-window",
|
||||||
is_flag=True,
|
flag_value="always",
|
||||||
help="Hide info window.",
|
help="Hide info window. By default, hide the info window if there is only one screen.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--show-info-window",
|
||||||
|
"hide_info_window",
|
||||||
|
flag_value="never",
|
||||||
|
help="Force to show info window.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--info-window-screen",
|
"--info-window-screen",
|
||||||
@ -231,11 +237,13 @@ def start_at_callback(
|
|||||||
metavar="NUMBER",
|
metavar="NUMBER",
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="Put info window on the given screen (a.k.a. display).",
|
help="Put info window on the given screen (a.k.a. display). "
|
||||||
|
"If there is more than one screen, it will by default put the info window "
|
||||||
|
"on a different screen than the main player.",
|
||||||
)
|
)
|
||||||
@click.help_option("-h", "--help")
|
@click.help_option("-h", "--help")
|
||||||
@verbosity_option
|
@verbosity_option
|
||||||
def present(
|
def present( # noqa: C901
|
||||||
scenes: list[str],
|
scenes: list[str],
|
||||||
config_path: Path,
|
config_path: Path,
|
||||||
folder: Path,
|
folder: Path,
|
||||||
@ -251,7 +259,7 @@ def present(
|
|||||||
screen_number: Optional[int],
|
screen_number: Optional[int],
|
||||||
playback_rate: float,
|
playback_rate: float,
|
||||||
next_terminates_loop: bool,
|
next_terminates_loop: bool,
|
||||||
hide_info_window: bool,
|
hide_info_window: Optional[Literal["always", "never"]],
|
||||||
info_window_screen_number: Optional[int],
|
info_window_screen_number: Optional[int],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -294,22 +302,36 @@ def present(
|
|||||||
app = qapp()
|
app = qapp()
|
||||||
app.setApplicationName("Manim Slides")
|
app.setApplicationName("Manim Slides")
|
||||||
|
|
||||||
|
screens = app.screens()
|
||||||
|
|
||||||
def get_screen(number: int) -> Optional[QScreen]:
|
def get_screen(number: int) -> Optional[QScreen]:
|
||||||
try:
|
try:
|
||||||
return app.screens()[number]
|
return screens[number]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Invalid screen number {number}, "
|
f"Invalid screen number {number}, "
|
||||||
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
|
f"allowed values are from 0 to {len(screens) - 1} (incl.)"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
should_hide_info_window = False
|
||||||
|
|
||||||
|
if hide_info_window is None:
|
||||||
|
should_hide_info_window = len(screens) == 1
|
||||||
|
elif hide_info_window == "always":
|
||||||
|
should_hide_info_window = True
|
||||||
|
|
||||||
|
if should_hide_info_window and info_window_screen_number is not None:
|
||||||
|
logger.warning(
|
||||||
|
f"Ignoring `--info-window-screen` because `--hide-info-window` is set to `{hide_info_window}`."
|
||||||
|
)
|
||||||
|
|
||||||
if screen_number is not None:
|
if screen_number is not None:
|
||||||
screen = get_screen(screen_number)
|
screen = get_screen(screen_number)
|
||||||
else:
|
else:
|
||||||
screen = None
|
screen = None
|
||||||
|
|
||||||
if info_window_screen_number is not None:
|
if info_window_screen_number is not None and not should_hide_info_window:
|
||||||
info_window_screen = get_screen(info_window_screen_number)
|
info_window_screen = get_screen(info_window_screen_number)
|
||||||
else:
|
else:
|
||||||
info_window_screen = None
|
info_window_screen = None
|
||||||
@ -333,11 +355,11 @@ def present(
|
|||||||
screen=screen,
|
screen=screen,
|
||||||
playback_rate=playback_rate,
|
playback_rate=playback_rate,
|
||||||
next_terminates_loop=next_terminates_loop,
|
next_terminates_loop=next_terminates_loop,
|
||||||
hide_info_window=hide_info_window,
|
hide_info_window=should_hide_info_window,
|
||||||
info_window_screen=info_window_screen,
|
info_window_screen=info_window_screen,
|
||||||
)
|
)
|
||||||
|
|
||||||
player.show()
|
player.show(screens)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
@ -28,7 +28,6 @@ class Info(QWidget): # type: ignore[misc]
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
full_screen: bool,
|
|
||||||
aspect_ratio_mode: Qt.AspectRatioMode,
|
aspect_ratio_mode: Qt.AspectRatioMode,
|
||||||
screen: Optional[QScreen],
|
screen: Optional[QScreen],
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -38,9 +37,6 @@ class Info(QWidget): # type: ignore[misc]
|
|||||||
self.setScreen(screen)
|
self.setScreen(screen)
|
||||||
self.move(screen.geometry().topLeft())
|
self.move(screen.geometry().topLeft())
|
||||||
|
|
||||||
if full_screen:
|
|
||||||
self.setWindowState(Qt.WindowFullScreen)
|
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
|
|
||||||
# Current slide view
|
# Current slide view
|
||||||
@ -243,7 +239,6 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
self.slide_changed.connect(self.slide_changed_callback)
|
self.slide_changed.connect(self.slide_changed_callback)
|
||||||
|
|
||||||
self.info = Info(
|
self.info = Info(
|
||||||
full_screen=full_screen,
|
|
||||||
aspect_ratio_mode=aspect_ratio_mode,
|
aspect_ratio_mode=aspect_ratio_mode,
|
||||||
screen=info_window_screen,
|
screen=info_window_screen,
|
||||||
)
|
)
|
||||||
@ -468,13 +463,13 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
def presentation_changed_callback(self) -> None:
|
def presentation_changed_callback(self) -> None:
|
||||||
index = self.current_presentation_index
|
index = self.current_presentation_index
|
||||||
count = self.presentations_count
|
count = self.presentations_count
|
||||||
self.info.scene_label.setText(f"{index+1:4d}/{count:4<d}")
|
self.info.scene_label.setText(f"{index + 1:4d}/{count:4<d}")
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def slide_changed_callback(self) -> None:
|
def slide_changed_callback(self) -> None:
|
||||||
index = self.current_slide_index
|
index = self.current_slide_index
|
||||||
count = self.current_slides_count
|
count = self.current_slides_count
|
||||||
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
|
self.info.slide_label.setText(f"{index + 1:4d}/{count:4<d}")
|
||||||
self.info.slide_notes.setText(self.current_slide_config.notes)
|
self.info.slide_notes.setText(self.current_slide_config.notes)
|
||||||
self.preview_next_slide()
|
self.preview_next_slide()
|
||||||
|
|
||||||
@ -484,11 +479,28 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
self.info.next_media_player.setSource(url)
|
self.info.next_media_player.setSource(url)
|
||||||
self.info.next_media_player.play()
|
self.info.next_media_player.play()
|
||||||
|
|
||||||
def show(self) -> None:
|
def show(self, screens: list[QScreen]) -> None:
|
||||||
|
"""Screens is necessary to prevent the info window from being shown on the same screen as the main window (especially in full screen mode)."""
|
||||||
super().show()
|
super().show()
|
||||||
|
|
||||||
if not self.hide_info_window:
|
if not self.hide_info_window:
|
||||||
self.info.show()
|
if len(screens) > 1 and self.isFullScreen():
|
||||||
|
self.ensure_different_screens(screens)
|
||||||
|
|
||||||
|
if self.isFullScreen():
|
||||||
|
self.info.showFullScreen()
|
||||||
|
else:
|
||||||
|
self.info.show()
|
||||||
|
|
||||||
|
if (
|
||||||
|
len(screens) > 1 and self.info.screen() == self.screen()
|
||||||
|
): # It is better when Qt assigns the location, but if it fails to, this is a fallback
|
||||||
|
self.ensure_different_screens(screens)
|
||||||
|
|
||||||
|
def ensure_different_screens(self, screens: list[QScreen]) -> None:
|
||||||
|
target_screen = screens[1] if self.screen() == screens[0] else screens[0]
|
||||||
|
self.info.setScreen(target_screen)
|
||||||
|
self.info.move(target_screen.geometry().topLeft())
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@ -515,6 +527,9 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def reverse(self) -> None:
|
def reverse(self) -> None:
|
||||||
|
if self.playing_reversed_slide and self.current_slide_index >= 1:
|
||||||
|
self.current_slide_index -= 1
|
||||||
|
|
||||||
self.load_reversed_slide()
|
self.load_reversed_slide()
|
||||||
self.preview_next_slide()
|
self.preview_next_slide()
|
||||||
|
|
||||||
@ -535,8 +550,10 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
def full_screen(self) -> None:
|
def full_screen(self) -> None:
|
||||||
if self.windowState() == Qt.WindowFullScreen:
|
if self.windowState() == Qt.WindowFullScreen:
|
||||||
self.setWindowState(Qt.WindowNoState)
|
self.setWindowState(Qt.WindowNoState)
|
||||||
|
self.info.setWindowState(Qt.WindowNoState)
|
||||||
else:
|
else:
|
||||||
self.setWindowState(Qt.WindowFullScreen)
|
self.setWindowState(Qt.WindowFullScreen)
|
||||||
|
self.info.setWindowState(Qt.WindowFullScreen)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def hide_mouse(self) -> None:
|
def hide_mouse(self) -> None:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Alias command to either
|
Alias command to either
|
||||||
``manim render [OPTIONS] [ARGS]...`` or
|
``manim render [OPTIONS] [ARGS]...`` or
|
||||||
``manimgl [OPTIONS] [ARGS]...``.
|
``manimgl -w [OPTIONS] [ARGS]...``.
|
||||||
|
|
||||||
This is especially useful for two reasons:
|
This is especially useful for two reasons:
|
||||||
|
|
||||||
@ -48,6 +48,6 @@ def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
|
|||||||
if ce and gl:
|
if ce and gl:
|
||||||
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
||||||
if gl:
|
if gl:
|
||||||
subprocess.run([sys.executable, "-m", "manimlib", *args])
|
subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
|
||||||
else:
|
else:
|
||||||
subprocess.run([sys.executable, "-m", "manim", "render", *args])
|
subprocess.run([sys.executable, "-m", "manim", "render", *args])
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
|
"API_NAME",
|
||||||
"MANIM",
|
"MANIM",
|
||||||
"MANIMGL",
|
"MANIMGL",
|
||||||
"API_NAME",
|
|
||||||
"Slide",
|
"Slide",
|
||||||
"ThreeDSlide",
|
"ThreeDSlide",
|
||||||
]
|
]
|
||||||
@ -35,7 +35,7 @@ API: str = os.environ.get(MANIM_API, "manim").lower()
|
|||||||
|
|
||||||
if API not in API_NAMES:
|
if API not in API_NAMES:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
f"Specified MANIM_API={API!r} is not in valid options: " f"{API_NAMES}",
|
f"Specified MANIM_API={API!r} is not in valid options: {API_NAMES}",
|
||||||
)
|
)
|
||||||
|
|
||||||
API_NAME = API_NAMES[API]
|
API_NAME = API_NAMES[API]
|
||||||
|
@ -2,7 +2,6 @@ from pathlib import Path
|
|||||||
from typing import Any, ClassVar, Optional
|
from typing import Any, ClassVar, Optional
|
||||||
|
|
||||||
from manimlib import Scene, ThreeDCamera
|
from manimlib import Scene, ThreeDCamera
|
||||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
|
||||||
|
|
||||||
from .base import BaseSlide
|
from .base import BaseSlide
|
||||||
|
|
||||||
@ -10,42 +9,40 @@ from .base import BaseSlide
|
|||||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
kwargs.setdefault("file_writer_config", {}).update(
|
kwargs.setdefault("file_writer_config", {}).update(
|
||||||
skip_animations=True,
|
subdivide_output=True,
|
||||||
break_into_partial_movies=True,
|
|
||||||
write_to_movie=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
kwargs["preview"] = False # Avoid opening a preview window
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _frame_height(self) -> float:
|
def _frame_height(self) -> float:
|
||||||
return self.camera.frame.get_height() # type: ignore
|
return float(self.camera.get_frame_height())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _frame_width(self) -> float:
|
def _frame_width(self) -> float:
|
||||||
return self.camera.frame.get_width() # type: ignore
|
return float(self.camera.get_frame_width())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _background_color(self) -> str:
|
def _background_color(self) -> str:
|
||||||
return self.camera_config["background_color"].hex # type: ignore
|
rgba = self.camera.background_rgba
|
||||||
|
r = int(255 * rgba[0])
|
||||||
|
g = int(255 * rgba[1])
|
||||||
|
b = int(255 * rgba[2])
|
||||||
|
if rgba[3] == 1.0:
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
|
a = int(255 * rgba[3])
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _resolution(self) -> tuple[int, int]:
|
def _resolution(self) -> tuple[int, int]:
|
||||||
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
|
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _partial_movie_files(self) -> list[Path]:
|
def _partial_movie_files(self) -> list[Path]:
|
||||||
kwargs = {
|
partial_movie_directory = self.file_writer.partial_movie_directory
|
||||||
"remove_non_integer_files": True,
|
extension = self.file_writer.movie_file_extension
|
||||||
"extension": self.file_writer.movie_file_extension,
|
return sorted(partial_movie_directory.glob(f"*{extension}"))
|
||||||
}
|
|
||||||
return [
|
|
||||||
Path(file)
|
|
||||||
for file in get_sorted_integer_files(
|
|
||||||
self.file_writer.partial_movie_directory, **kwargs
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _show_progress_bar(self) -> bool:
|
def _show_progress_bar(self) -> bool:
|
||||||
|
@ -19,31 +19,32 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="reveal">
|
<div class="reveal">
|
||||||
<div class="slides">
|
<div class="slides">
|
||||||
{%- for presentation_config in presentation_configs -%}
|
{% for presentation_config in presentation_configs -%}
|
||||||
{% set outer_loop = loop %}
|
{% set outer_loop = loop %}
|
||||||
{%- for slide_config in presentation_config.slides -%}
|
{%- for slide_config in presentation_config.slides -%}
|
||||||
{%- if data_uri -%}
|
{%- if one_file -%}
|
||||||
{% set file = file_to_data_uri(slide_config.file) %}
|
{% set file = file_to_data_uri(slide_config.file) %}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{% set file = assets_dir / slide_config.file.name %}
|
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<section
|
<section
|
||||||
data-background-size={{ background_size }}
|
data-background-size={{ background_size }}
|
||||||
data-background-color="{{ presentation_config.background_color }}"
|
data-background-color="{{ presentation_config.background_color }}"
|
||||||
data-background-video="{{ file }}"
|
data-background-video="{{ file }}"
|
||||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||||
data-background-video-muted
|
data-background-video-muted
|
||||||
{%- endif %}
|
{%- endif -%}
|
||||||
{% if slide_config.loop -%}
|
{% if slide_config.loop -%}
|
||||||
data-background-video-loop
|
data-background-video-loop
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% if slide_config.auto_next -%}
|
{% if slide_config.auto_next -%}
|
||||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||||
{%- endif -%}>
|
{%- endif %}
|
||||||
{% if slide_config.notes != "" -%}
|
>
|
||||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
{%- if slide_config.notes != "" -%}
|
||||||
{%- endif %}
|
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||||
</section>
|
{%- endif %}
|
||||||
|
</section>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</div>
|
</div>
|
||||||
@ -128,6 +129,9 @@
|
|||||||
// Flags if we should monitor the hash and change slides accordingly
|
// Flags if we should monitor the hash and change slides accordingly
|
||||||
respondToHashChanges: {{ respond_to_hash_changes }},
|
respondToHashChanges: {{ respond_to_hash_changes }},
|
||||||
|
|
||||||
|
// Enable support for jump-to-slide navigation shortcuts
|
||||||
|
jumpToSlide: {{ jump_to_slide }},
|
||||||
|
|
||||||
// Push each slide change to the browser history. Implies `hash: true`
|
// Push each slide change to the browser history. Implies `hash: true`
|
||||||
history: {{ history }},
|
history: {{ history }},
|
||||||
|
|
||||||
@ -316,27 +320,48 @@
|
|||||||
hideCursorTime: {{ hide_cursor_time }}
|
hideCursorTime: {{ hide_cursor_time }}
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if data_uri -%}
|
{% if one_file -%}
|
||||||
// Fix found by @t-fritsch on GitHub
|
// Fix found by @t-fritsch and @Rapsssito on GitHub
|
||||||
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
|
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
|
||||||
function fixBase64VideoBackground(event) {
|
function setVideoBase64(video) {
|
||||||
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
|
const sources = video.querySelectorAll('source');
|
||||||
if (event.currentSlide.getAttribute('data-background-video')) {
|
// Update the source of the video
|
||||||
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
|
sources.forEach((source, i) => {
|
||||||
video = background.querySelector('video'),
|
const src = source.getAttribute('src');
|
||||||
sources = video.querySelectorAll('source');
|
if(src.match(/^data:video.*;base64$/)) {
|
||||||
|
const nextSrc = sources[i+1]?.getAttribute('src');
|
||||||
|
video.setAttribute('src', `${src},${nextSrc}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sources.forEach((source, i) => {
|
function fixBase64VideoBackground(event) {
|
||||||
const src = source.getAttribute('src');
|
// Analyze all slides backgrounds
|
||||||
if(src.match(/^data:video.*;base64$/)) {
|
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
|
||||||
const nextSrc = sources[i+1]?.getAttribute('src');
|
// Get the slide video and its sources for each background
|
||||||
video.setAttribute('src', `${src},${nextSrc}`);
|
const video = slide.querySelector('video');
|
||||||
}
|
if (video) {
|
||||||
});
|
setVideoBase64(video);
|
||||||
|
} else {
|
||||||
|
// Listen to the creation of the video element
|
||||||
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
for (const addedNode of mutation.addedNodes) {
|
||||||
|
if (addedNode.tagName === 'VIDEO') {
|
||||||
|
setVideoBase64(addedNode);
|
||||||
|
observer.disconnect(); // Stop observing once the video is handled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(slide, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Setup base64 videos
|
||||||
Reveal.on( 'ready', fixBase64VideoBackground );
|
Reveal.on( 'ready', fixBase64VideoBackground );
|
||||||
Reveal.on( 'slidechanged', fixBase64VideoBackground );
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ classifiers = [
|
|||||||
"Topic :: Scientific/Engineering",
|
"Topic :: Scientific/Engineering",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"av>=9.0.0",
|
"av>=9.0.0,<14",
|
||||||
|
"beautifulsoup4>=4.12.3",
|
||||||
"click>=8.1.3",
|
"click>=8.1.3",
|
||||||
"click-default-group>=1.2.2",
|
"click-default-group>=1.2.2",
|
||||||
"jinja2>=3.1.2",
|
"jinja2>=3.1.2",
|
||||||
@ -32,7 +33,6 @@ dependencies = [
|
|||||||
"rich>=13.3.2",
|
"rich>=13.3.2",
|
||||||
"rtoml>=0.11.0",
|
"rtoml>=0.11.0",
|
||||||
"tqdm>=4.64.1",
|
"tqdm>=4.64.1",
|
||||||
"trogon>=0.6.0",
|
|
||||||
]
|
]
|
||||||
description = "Tool for live presentations using manim"
|
description = "Tool for live presentations using manim"
|
||||||
dynamic = ["readme", "version"]
|
dynamic = ["readme", "version"]
|
||||||
@ -49,7 +49,10 @@ docs = [
|
|||||||
"myst-parser>=2.0.0",
|
"myst-parser>=2.0.0",
|
||||||
"nbsphinx>=0.9.2",
|
"nbsphinx>=0.9.2",
|
||||||
"pandoc>=2.3",
|
"pandoc>=2.3",
|
||||||
|
"pygments<2.19", # See: https://github.com/ManimCommunity/manim/issues/4104
|
||||||
"sphinx>=7.0.1",
|
"sphinx>=7.0.1",
|
||||||
|
"sphinxcontrib-programoutput>=0.18",
|
||||||
|
"sphinx-design>=0.6.1",
|
||||||
"sphinx-click>=4.4.0",
|
"sphinx-click>=4.4.0",
|
||||||
"sphinx-copybutton>=0.5.1",
|
"sphinx-copybutton>=0.5.1",
|
||||||
"sphinxext-opengraph>=0.7.5",
|
"sphinxext-opengraph>=0.7.5",
|
||||||
@ -58,11 +61,11 @@ full = [
|
|||||||
"manim-slides[magic,manim,sphinx-directive]",
|
"manim-slides[magic,manim,sphinx-directive]",
|
||||||
]
|
]
|
||||||
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
|
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
|
||||||
manim = ["manim>=0.18.0"]
|
manim = ["manim>=0.17"]
|
||||||
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
|
manimgl = ["manimgl>=1.7.2"]
|
||||||
pyqt6 = ["pyqt6>=6.7.0"]
|
pyqt6 = ["pyqt6>=6.7.0"]
|
||||||
pyqt6-full = ["manim-slides[full,pyqt6]"]
|
pyqt6-full = ["manim-slides[full,pyqt6]"]
|
||||||
pyside6 = ["pyside6>=6.6.1"]
|
pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
|
||||||
pyside6-full = ["manim-slides[full,pyside6]"]
|
pyside6-full = ["manim-slides[full,pyside6]"]
|
||||||
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
||||||
tests = [
|
tests = [
|
||||||
@ -88,7 +91,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
|
|||||||
allow_dirty = false
|
allow_dirty = false
|
||||||
commit = true
|
commit = true
|
||||||
commit_args = ""
|
commit_args = ""
|
||||||
current_version = "5.1.7"
|
current_version = "5.3.1"
|
||||||
ignore_missing_version = false
|
ignore_missing_version = false
|
||||||
message = "chore(deps): bump version from {current_version} to {new_version}"
|
message = "chore(deps): bump version from {current_version} to {new_version}"
|
||||||
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
|
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
|
||||||
@ -134,6 +137,13 @@ replace = '''<!-- start changelog -->
|
|||||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)'''
|
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)'''
|
||||||
search = "<!-- start changelog -->"
|
search = "<!-- start changelog -->"
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "uv.lock"
|
||||||
|
replace = '''name = "manim-slides"
|
||||||
|
version = "{new_version}"'''
|
||||||
|
search = '''name = "manim-slides"
|
||||||
|
version = "{current_version}"'''
|
||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
|
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
|
||||||
check-hidden = true
|
check-hidden = true
|
||||||
@ -190,7 +200,8 @@ filterwarnings = [
|
|||||||
'''ignore:'audioop' is deprecated:DeprecationWarning''',
|
'''ignore:'audioop' is deprecated:DeprecationWarning''',
|
||||||
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
|
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
|
||||||
'ignore::DeprecationWarning:pkg_resources.*:',
|
'ignore::DeprecationWarning:pkg_resources.*:',
|
||||||
'ignore::DeprecationWarning:pydub.*:',
|
'ignore:invalid escape sequence.*:DeprecationWarning',
|
||||||
|
'ignore:invalid escape sequence.*:SyntaxWarning',
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
@ -227,9 +238,3 @@ dev-dependencies = [
|
|||||||
"pre-commit>=3.5.0",
|
"pre-commit>=3.5.0",
|
||||||
"setuptools>=73.0.1",
|
"setuptools>=73.0.1",
|
||||||
]
|
]
|
||||||
override-dependencies = [
|
|
||||||
# Bypass constraints from ManimGL
|
|
||||||
"manimpango>=0.5.0,<1.0.0",
|
|
||||||
"numpy<=1.24;python_version < '3.12'",
|
|
||||||
"numpy>=1.26;python_version >= '3.12'",
|
|
||||||
]
|
|
||||||
|
@ -39,6 +39,13 @@ def test_checkhealth(
|
|||||||
del sys.modules["qtpy"] # Avoid using cached module
|
del sys.modules["qtpy"] # Avoid using cached module
|
||||||
|
|
||||||
with missing_modules(*names):
|
with missing_modules(*names):
|
||||||
|
if (
|
||||||
|
not manimlib_missing
|
||||||
|
and not MANIMGL_NOT_INSTALLED
|
||||||
|
and sys.version_info < (3, 10)
|
||||||
|
):
|
||||||
|
pytest.skip("See https://github.com/3b1b/manim/issues/2263")
|
||||||
|
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
checkhealth,
|
checkhealth,
|
||||||
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
|
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
|
||||||
|
@ -3,7 +3,8 @@ from enum import EnumMeta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from manim_slides.config import PresentationConfig
|
from manim_slides.config import PresentationConfig
|
||||||
from manim_slides.convert import (
|
from manim_slides.convert import (
|
||||||
@ -156,6 +157,119 @@ class TestConverter:
|
|||||||
file_contents = out_file.read_text()
|
file_contents = out_file.read_text()
|
||||||
assert "manim" in file_contents.casefold()
|
assert "manim" in file_contents.casefold()
|
||||||
|
|
||||||
|
def test_revealjs_offline_converter(
|
||||||
|
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||||
|
) -> None:
|
||||||
|
out_file = tmp_path / "slides.html"
|
||||||
|
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
|
||||||
|
out_file
|
||||||
|
)
|
||||||
|
assert out_file.exists()
|
||||||
|
assets_dir = Path(tmp_path / "slides_assets")
|
||||||
|
assert assets_dir.is_dir()
|
||||||
|
for file in [
|
||||||
|
"black.min.css",
|
||||||
|
"reveal.min.css",
|
||||||
|
"reveal.min.js",
|
||||||
|
"zenburn.min.css",
|
||||||
|
]:
|
||||||
|
assert (assets_dir / file).exists()
|
||||||
|
|
||||||
|
def test_revealjs_data_encode(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
presentation_config: PresentationConfig,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# Mock requests.Session.get to return a fake response (should not be called)
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, content: bytes, text: str, status_code: int) -> None:
|
||||||
|
self.content = content
|
||||||
|
self.text = text
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
monkeypatch.setattr(
|
||||||
|
requests.Session,
|
||||||
|
"get",
|
||||||
|
lambda self, url: MockResponse(
|
||||||
|
b"body { background-color: #9a3241; }",
|
||||||
|
"body { background-color: #9a3241; }",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
out_file = tmp_path / "slides.html"
|
||||||
|
RevealJS(
|
||||||
|
presentation_configs=[presentation_config], offline="false", one_file="true"
|
||||||
|
).convert_to(out_file)
|
||||||
|
assert out_file.exists()
|
||||||
|
# Check that assets are not stored
|
||||||
|
assert not (tmp_path / "slides_assets").exists()
|
||||||
|
|
||||||
|
with open(out_file, encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
|
||||||
|
# Check if video is encoded in base64
|
||||||
|
videos = soup.find_all("section")
|
||||||
|
assert all(
|
||||||
|
"data:video/mp4;base64," in video["data-background-video"]
|
||||||
|
for video in videos
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if CSS is not inlined
|
||||||
|
styles = soup.find_all("style")
|
||||||
|
assert not any("background-color: #9a3241;" in style.string for style in styles)
|
||||||
|
# Check if JS is not inlined
|
||||||
|
scripts = soup.find_all("script")
|
||||||
|
assert not any(
|
||||||
|
"background-color: #9a3241;" in (script.string or "") for script in scripts
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_revealjs_offline_inlining(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
presentation_config: PresentationConfig,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# Mock requests.Session.get to return a fake response
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, content: bytes, text: str, status_code: int) -> None:
|
||||||
|
self.content = content
|
||||||
|
self.text = text
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
monkeypatch.setattr(
|
||||||
|
requests.Session,
|
||||||
|
"get",
|
||||||
|
lambda self, url: MockResponse(
|
||||||
|
b"body { background-color: #9a3241; }",
|
||||||
|
"body { background-color: #9a3241; }",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
out_file = tmp_path / "slides.html"
|
||||||
|
RevealJS(
|
||||||
|
presentation_configs=[presentation_config], offline="true", one_file="true"
|
||||||
|
).convert_to(out_file)
|
||||||
|
assert out_file.exists()
|
||||||
|
|
||||||
|
with open(out_file, encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
|
||||||
|
# Check if CSS is inlined
|
||||||
|
styles = soup.find_all("style")
|
||||||
|
assert any("background-color: #9a3241;" in style.string for style in styles)
|
||||||
|
|
||||||
|
# Check if JS is inlined
|
||||||
|
scripts = soup.find_all("script")
|
||||||
|
assert any("background-color: #9a3241;" in script.string for script in scripts)
|
||||||
|
|
||||||
def test_htmlzip_converter(
|
def test_htmlzip_converter(
|
||||||
self, tmp_path: Path, presentation_config: PresentationConfig
|
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -208,10 +322,6 @@ class TestConverter:
|
|||||||
).convert_to(out_file)
|
).convert_to(out_file)
|
||||||
assert out_file.exists()
|
assert out_file.exists()
|
||||||
|
|
||||||
def test_converter_no_presentation_config(self) -> None:
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
Converter(presentation_configs=[])
|
|
||||||
|
|
||||||
def test_pptx_converter(
|
def test_pptx_converter(
|
||||||
self, tmp_path: Path, presentation_config: PresentationConfig
|
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -64,6 +65,34 @@ def test_convert(slides_folder: Path, extension: str) -> None:
|
|||||||
assert results.exit_code == 0
|
assert results.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("extension",), [("html",)])
|
||||||
|
def test_convert_data_uri_deprecated(slides_folder: Path, extension: str) -> None:
|
||||||
|
runner = CliRunner(mix_stderr=False)
|
||||||
|
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
results = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"convert",
|
||||||
|
"BasicSlide",
|
||||||
|
f"basic_example.{extension}",
|
||||||
|
"--folder",
|
||||||
|
str(slides_folder),
|
||||||
|
"--to",
|
||||||
|
extension,
|
||||||
|
"-cdata_uri=true",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert any(
|
||||||
|
"'data_uri' configuration option is deprecated" in str(item.message)
|
||||||
|
and item.category is DeprecationWarning
|
||||||
|
for item in w
|
||||||
|
)
|
||||||
|
assert results.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("extension", "expected_log"),
|
("extension", "expected_log"),
|
||||||
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],
|
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],
|
||||||
|
@ -6,6 +6,11 @@ import pytest
|
|||||||
|
|
||||||
import manim_slides.slide as slide
|
import manim_slides.slide as slide
|
||||||
|
|
||||||
|
skip_if_py39 = pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 10),
|
||||||
|
reason="See https://github.com/3b1b/manim/issues/2263",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_import(
|
def assert_import(
|
||||||
*,
|
*,
|
||||||
@ -20,6 +25,7 @@ def assert_import(
|
|||||||
assert slide.MANIMGL == manimgl
|
assert slide.MANIMGL == manimgl
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_py39
|
||||||
def test_force_api() -> None:
|
def test_force_api() -> None:
|
||||||
pytest.importorskip("manimlib")
|
pytest.importorskip("manimlib")
|
||||||
import manim # noqa: F401
|
import manim # noqa: F401
|
||||||
@ -53,6 +59,7 @@ def test_invalid_api() -> None:
|
|||||||
del os.environ[slide.MANIM_API]
|
del os.environ[slide.MANIM_API]
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_py39
|
||||||
@pytest.mark.filterwarnings("ignore:assert_import")
|
@pytest.mark.filterwarnings("ignore:assert_import")
|
||||||
def test_manim_and_manimgl_imported() -> None:
|
def test_manim_and_manimgl_imported() -> None:
|
||||||
pytest.importorskip("manimlib")
|
pytest.importorskip("manimlib")
|
||||||
@ -79,6 +86,7 @@ def test_manim_imported() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_py39
|
||||||
def test_manimgl_imported() -> None:
|
def test_manimgl_imported() -> None:
|
||||||
pytest.importorskip("manimlib")
|
pytest.importorskip("manimlib")
|
||||||
import manimlib # noqa: F401
|
import manimlib # noqa: F401
|
||||||
|
@ -26,23 +26,32 @@ from manim_slides.defaults import FOLDER_PATH
|
|||||||
from manim_slides.render import render
|
from manim_slides.render import render
|
||||||
from manim_slides.slide.manim import Slide as CESlide
|
from manim_slides.slide.manim import Slide as CESlide
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
|
||||||
|
class _GLSlide:
|
||||||
|
def construct(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
GLSlide = pytest.param(
|
||||||
|
_GLSlide,
|
||||||
|
marks=pytest.mark.skip(reason="See https://github.com/3b1b/manim/issues/2263"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from manim_slides.slide.manimlib import Slide as GLSlide
|
||||||
|
|
||||||
|
_GLSlide = GLSlide
|
||||||
|
|
||||||
|
|
||||||
class CEGLSlide(CESlide):
|
class CEGLSlide(CESlide):
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
|
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 12):
|
SlideType = Union[type[CESlide], type[_GLSlide], type[CEGLSlide]]
|
||||||
|
Slide = Union[CESlide, _GLSlide, CEGLSlide]
|
||||||
class _GLSlide:
|
|
||||||
pass
|
|
||||||
|
|
||||||
GLSlide = pytest.param(_GLSlide, marks=pytest.mark.skip())
|
|
||||||
else:
|
|
||||||
from manim_slides.slide.manimlib import Slide as GLSlide
|
|
||||||
|
|
||||||
SlideType = Union[type[CESlide], type[GLSlide], type[CEGLSlide]]
|
|
||||||
Slide = Union[CESlide, GLSlide, CEGLSlide]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -52,8 +61,8 @@ Slide = Union[CESlide, GLSlide, CEGLSlide]
|
|||||||
pytest.param(
|
pytest.param(
|
||||||
"--GL",
|
"--GL",
|
||||||
marks=pytest.mark.skipif(
|
marks=pytest.mark.skipif(
|
||||||
sys.version_info >= (3, 12),
|
sys.version_info < (3, 10),
|
||||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -161,8 +170,8 @@ def test_clear_cache(
|
|||||||
pytest.param(
|
pytest.param(
|
||||||
"--GL",
|
"--GL",
|
||||||
marks=pytest.mark.skipif(
|
marks=pytest.mark.skipif(
|
||||||
sys.version_info >= (3, 12),
|
sys.version_info < (3, 10),
|
||||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -203,13 +212,10 @@ def init_slide(cls: SlideType) -> Slide:
|
|||||||
if issubclass(cls, CESlide):
|
if issubclass(cls, CESlide):
|
||||||
return cls()
|
return cls()
|
||||||
elif issubclass(cls, GLSlide):
|
elif issubclass(cls, GLSlide):
|
||||||
from manimlib.config import get_configuration, parse_cli
|
from manimlib.config import parse_cli
|
||||||
from manimlib.extract_scene import get_scene_config
|
|
||||||
|
|
||||||
args = parse_cli()
|
_args = parse_cli()
|
||||||
config = get_configuration(args)
|
return cls()
|
||||||
scene_config = get_scene_config(config)
|
|
||||||
return cls(**scene_config)
|
|
||||||
|
|
||||||
raise ValueError(f"Unsupported class {cls}")
|
raise ValueError(f"Unsupported class {cls}")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user