mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 19:16:21 +08:00
Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
fb562d88ac | |||
8a1fb4c259 | |||
9db2358793 | |||
de91ac7b7c | |||
6ad89ecdf6 | |||
c98501b71c | |||
2d7d097276 | |||
eeac42c638 | |||
ae42403962 | |||
1539b2f7e4 | |||
a39a9c2bb1 | |||
87867762ea | |||
2f4fe9bd06 | |||
924d8210d9 | |||
59236b84db | |||
c047da67b1 | |||
5b6f5eb1e4 | |||
3b23c7739d | |||
517fbc6240 | |||
c3e1aa0276 | |||
3e7174c331 | |||
6572dc3e21 | |||
f707b6cd0c | |||
98f6dbd864 | |||
8c0dd6a605 | |||
ba3756b314 | |||
1413a5eb9e | |||
467f744450 | |||
4fe347e66a | |||
9b62b62386 | |||
e2d8c5667f | |||
e80d1d08eb | |||
5924c329a8 | |||
20f91a4590 | |||
964de66563 | |||
89377e3a55 | |||
0aa624c8ad | |||
9460f6b135 | |||
c0a240d758 | |||
b08073983b | |||
993acf0e3f | |||
962ff2bffe | |||
9f2e4757ec | |||
6d85222fdc | |||
19f60bf880 | |||
554f076a25 | |||
676acc6e3b | |||
26c3fdca75 | |||
f17e855323 | |||
73490298b3 | |||
c9a06e45aa | |||
45269b9d1c | |||
14df6eb55a | |||
ef014ebac6 | |||
d5d1513d94 | |||
bd04dae2bf | |||
d5c20d1791 | |||
527ce2767e | |||
04bca458f1 | |||
4e7abe8706 | |||
49e2c31d9a | |||
5920a843f5 | |||
59dd365291 | |||
3e2e64b09f | |||
8a3bf87db8 | |||
498e9af2bf | |||
24ee23af11 | |||
a775c4989b | |||
04f6ee7f9b | |||
bbc539b461 | |||
2d9d263c9c | |||
cfa9c082ab | |||
67533c460e | |||
a85f1c4036 | |||
b3fe6f17b9 | |||
e7182a445d |
@ -1,16 +0,0 @@
|
||||
[bumpversion]
|
||||
current_version = 5.1.3
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?
|
||||
serialize =
|
||||
{major}.{minor}.{patch}-rc{release}
|
||||
{major}.{minor}.{patch}
|
||||
commit = True
|
||||
message = chore(version): bump {current_version} to {new_version}
|
||||
|
||||
[bumpversion:file:manim_slides/__version__.py]
|
||||
search = __version__ = "{current_version}"
|
||||
replace = __version__ = "{new_version}"
|
||||
|
||||
[bumpversion:file:CITATION.cff]
|
||||
search = version: v{current_version}
|
||||
replace = version: v{new_version}
|
161
.github/ISSUE_TEMPLATE/bug.yml
vendored
161
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -1,40 +1,177 @@
|
||||
name: Bug
|
||||
description: Report an issue to help improve the project.
|
||||
labels: bug
|
||||
title: '[BUG] <description>'
|
||||
title: '[BUG] <short-description-here>'
|
||||
labels: [bug]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
id: preamble
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for reporting a problem about Manim Slides!**
|
||||
|
||||
If you know how to solve your problem, feel free to submit a PR too!
|
||||
|
||||
> [!WARNING]
|
||||
> Before reporting your bug, please make sure to:
|
||||
>
|
||||
> 1. create and activate virtual environment (venv);
|
||||
> 2. install `manim-slides` and the necessary dependencies;
|
||||
> 3. and reduce your Python to a minimal working example (MWE).
|
||||
>
|
||||
> You can skip the last step if your issue occurs during installation.
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Terms
|
||||
description: 'By submitting this issue, I have:'
|
||||
options:
|
||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||
required: true
|
||||
- label: Checked the [frequently qsked questions]](https://manim-slides.eertmans.be/latest/faq.html);
|
||||
required: true
|
||||
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
|
||||
required: true
|
||||
- label: Created a virtual environment in which I can reproduce my bug;
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A brief description of the question or issue, also include what you tried and what didn't work
|
||||
label: Describe the issue
|
||||
description: A description of the issue, also include what you tried and what didn't work.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
|
||||
- type: input
|
||||
id: command
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
|
||||
label: Command
|
||||
description: |
|
||||
Enter the command that failed.
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
placeholder: manim-slides render mwe.py MWE
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue Type
|
||||
description: >
|
||||
Please select the option in the drop-down.
|
||||
options:
|
||||
- Installation issue
|
||||
- Visual bug when presenting (`manim-slides present`)
|
||||
- Bug when presenting with HTML/PowerPoint/... format (`manim-slides convert`)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: py-version
|
||||
attributes:
|
||||
label: Python version
|
||||
description: |
|
||||
Please copy and paste the output of `python --version`.
|
||||
Make sure to activate your virtual environment first (if any).
|
||||
placeholder: Python 3.11.8
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: venv
|
||||
attributes:
|
||||
label: Python environment
|
||||
description: |
|
||||
Please copy and paste the output of `manim-slides checkhealth`.
|
||||
Make sure to activate your virtual environment first (if any).
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
If Manim Slides installation failed, enter 'N/A' instead.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: What is your platform. Linux, macOS, or Windows?
|
||||
label: What is your platform?
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- Other (please precise below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: platform-other
|
||||
attributes:
|
||||
label: Other platform
|
||||
description: Please answer if you have replied *Other* above.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: Manim Slides Python code
|
||||
description: |
|
||||
Please copy and paste a minimal working example (MWE) of your Python code that can reproduce your bug.
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
placeholder: |
|
||||
from manim import *
|
||||
from manim_slides.slide import Slide
|
||||
|
||||
|
||||
class MWE(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=2, color=RED)
|
||||
dot = Dot()
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=0.5)
|
||||
self.next_slide()
|
||||
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
render: python
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Please copy and paste any relevant log output.
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable
|
||||
description: Please add screenshots if applicable.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
id: extra-info
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else we should know about this bug?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: suggested-fix
|
||||
attributes:
|
||||
label: Recommended fix or suggestions
|
||||
description: A clear and concise description of how you want to update it.
|
||||
validations:
|
||||
required: false
|
||||
|
56
.github/ISSUE_TEMPLATE/documentation.yml
vendored
56
.github/ISSUE_TEMPLATE/documentation.yml
vendored
@ -1,57 +1,61 @@
|
||||
name: Documentation
|
||||
description: Ask / Report an issue related to the documentation.
|
||||
title: 'DOC: <description>'
|
||||
labels: [bug, docs]
|
||||
title: '[DOC] <short-description-here>'
|
||||
labels: [documentation]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
id: preamble
|
||||
attributes:
|
||||
value: >
|
||||
**Thank you for wanting to report a problem with manim-slides docs!**
|
||||
value: |
|
||||
**Thank you for reporting a problem about Manim Slides' documentation!**
|
||||
|
||||
If you know how to solve your problem, feel free to submit a PR too!
|
||||
|
||||
If the problem seems straightforward, feel free to submit a PR instead!
|
||||
|
||||
|
||||
⚠
|
||||
Verify first that your issue is not already reported on GitHub [Issues].
|
||||
|
||||
|
||||
[Issues]:
|
||||
https://github.com/jeertmans/manim-slides/issues
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Terms
|
||||
description: 'By submitting this issue, I have:'
|
||||
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;
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the Issue
|
||||
label: Describe the issue
|
||||
description: A clear and concise description of the issue you encountered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
- type: textarea
|
||||
id: pages
|
||||
attributes:
|
||||
label: Affected Page
|
||||
description: Add a link to page with the problem.
|
||||
label: Affected page(s)
|
||||
description: Link to page(s) with the problem.
|
||||
placeholder: |
|
||||
+ https://manim-slides.eertmans.be/latest/installation.html
|
||||
+ https://manim-slides.eertmans.be/latest/features_table.html
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue Type
|
||||
label: Issue type
|
||||
description: >
|
||||
Please select the option in the drop-down.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<em>Issue?</em>
|
||||
</summary>
|
||||
</details>
|
||||
options:
|
||||
- Documentation Enhancement
|
||||
- Documentation Report
|
||||
- Typo, spelling mistake, broken link, etc.
|
||||
- Something is missing
|
||||
- Documentation enhancement
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggested-fix
|
||||
attributes:
|
||||
label: Recommended fix or suggestions
|
||||
description: A clear and concise description of how you want to update it.
|
||||
|
26
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
26
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,8 +1,26 @@
|
||||
name: Feature Request
|
||||
name: Feature request
|
||||
description: Have a new idea/feature? Please suggest!
|
||||
labels: enhancement
|
||||
title: '[FEATURE] <description>'
|
||||
title: '[FEATURE] <short-description-here>'
|
||||
labels: [enhancement]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
id: preamble
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for suggesting a new feature!**
|
||||
|
||||
If you know how to implement it, feel free to submit a PR too!
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Terms
|
||||
description: 'By submitting this issue, I have:'
|
||||
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;
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@ -10,6 +28,7 @@ body:
|
||||
description: A brief description of the enhancement you propose, also include what you tried and what worked.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
@ -17,6 +36,7 @@ body:
|
||||
description: Please add screenshots if applicable
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
attributes:
|
||||
|
14
.github/ISSUE_TEMPLATE/support.yml
vendored
14
.github/ISSUE_TEMPLATE/support.yml
vendored
@ -1,14 +0,0 @@
|
||||
name: Question/Help/Support
|
||||
description: Ask us about Manim Slides
|
||||
title: 'Support: Ask us anything'
|
||||
labels: [help, question]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Please explain the issue you're experiencing (with as much detail as possible):"
|
||||
description: >
|
||||
Please make sure to leave a reference to the document/code you're
|
||||
referring to.
|
||||
validations:
|
||||
required: true
|
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@ -10,11 +10,13 @@
|
||||
|
||||
<!-- Describe all the proposed changes in your PR -->
|
||||
|
||||
## Check List (Check all the applicable boxes)
|
||||
## Check List
|
||||
|
||||
- [ ] I understand that my contributions needs to pass the checks.
|
||||
- [ ] If I created new functions / methods, I documented them and add type hints.
|
||||
- [ ] If I modified already existing code, I updated the documentation accordingly.
|
||||
Check all the applicable boxes:
|
||||
|
||||
- [ ] I understand that my contributions needs to pass the checks;
|
||||
- [ ] If I created new functions / methods, I documented them and add type hints;
|
||||
- [ ] If I modified already existing code, I updated the documentation accordingly;
|
||||
- [ ] The title of my pull request is a short description of the requested changes.
|
||||
|
||||
## Screenshots
|
||||
|
27
.github/scripts/check_github_issues.py
vendored
Normal file
27
.github/scripts/check_github_issues.py
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
"""Check that GitHub issues (and PR) links match the number in Markdown link."""
|
||||
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = re.compile(
|
||||
r"\[#(?P<number1>[0-9]+)\]"
|
||||
r"\(https://github\.com/"
|
||||
r"(?:[a-zA-Z0-9_-]+)/(?:[a-zA-Z0-9_-]+)/"
|
||||
r"(?:(?:issues)|(?:pull))/(?P<number2>[0-9]+)\)"
|
||||
)
|
||||
|
||||
ret_code = 0
|
||||
|
||||
for glob_pattern in sys.argv[1:]:
|
||||
for file in glob.glob(glob_pattern, recursive=True):
|
||||
with open(file) as f:
|
||||
for i, line in enumerate(f):
|
||||
for m in p.finditer(line):
|
||||
if m.group("number1") != m.group("number2"):
|
||||
start, end = m.span()
|
||||
print(f"{file}:{i}: ", line[start:end], file=sys.stderr) # noqa: T201
|
||||
ret_code = 1
|
||||
|
||||
sys.exit(ret_code)
|
14
.github/workflows/publish.yml
vendored
14
.github/workflows/publish.yml
vendored
@ -18,15 +18,17 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install PDM
|
||||
uses: pdm-project/setup-pdm@v4
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: true
|
||||
enable-cache: true
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
run: pdm publish
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
publish-docker:
|
||||
name: Publish Docker image
|
||||
@ -61,7 +63,7 @@ jobs:
|
||||
print(f"tag_name={ref_tag}", file=f)
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
file: docker/Dockerfile
|
||||
|
99
.github/workflows/tests.yml
vendored
99
.github/workflows/tests.yml
vendored
@ -7,7 +7,52 @@ on:
|
||||
|
||||
name: Tests
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pip-install:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
pyversion: ['3.9', '3.10', '3.11', '3.12']
|
||||
extras: [pyside6-full, manimgl]
|
||||
exclude:
|
||||
- pyversion: '3.12'
|
||||
extras: manimgl
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.pyversion }}
|
||||
cache: pip
|
||||
|
||||
- name: Install manim dependencies on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
||||
|
||||
- name: Install manim dependencies on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||
nohup Xvfb $DISPLAY &
|
||||
|
||||
- name: Install Windows dependencies
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: choco install ffmpeg
|
||||
|
||||
- name: Install package
|
||||
shell: bash
|
||||
env:
|
||||
extras: ${{ matrix.extras }}
|
||||
run: pip install ".[$extras]"
|
||||
pytest:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -18,45 +63,29 @@ jobs:
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
MANIM_SLIDES_VERBOSITY: error
|
||||
PYTHONFAULTHANDLER: 1
|
||||
DISPLAY: :99
|
||||
GITHUB_WORKFLOWS: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install PDM
|
||||
uses: pdm-project/setup-pdm@v4
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
python-version: ${{ matrix.pyversion }}
|
||||
cache: true
|
||||
enable-cache: true
|
||||
|
||||
# Path related stuff
|
||||
- name: Append to Path on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
echo "${HOME}/.local/bin" >> $GITHUB_PATH
|
||||
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
|
||||
- name: Setup Python ${{ matrix.pyversion }}
|
||||
run: uv python install ${{ matrix.pyversion }}
|
||||
|
||||
- name: Append to Path on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Append to Path on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
# OS depedencies
|
||||
- name: Install manim dependencies on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install ffmpeg py3cairo
|
||||
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
||||
|
||||
- name: Install manim dependencies on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
|
||||
sudo apt-get install xvfb
|
||||
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||
nohup Xvfb $DISPLAY &
|
||||
|
||||
- name: Install Windows dependencies
|
||||
@ -68,21 +97,27 @@ jobs:
|
||||
uses: ssciwr/setup-mesa-dist-win@v2
|
||||
|
||||
- name: Install Manim Slides
|
||||
run: |
|
||||
pdm sync -Ggithub-action -Gtest
|
||||
run: uv sync --locked --extra tests
|
||||
|
||||
- name: Run pytest
|
||||
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
|
||||
run: pdm run pytest
|
||||
|
||||
- name: Run pytest and coverage
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
|
||||
run: pdm run pytest --cov-report xml --cov=manim_slides tests/
|
||||
run: uv run pytest
|
||||
|
||||
- name: Upload to codecov.io
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check links
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: yes
|
||||
use-verbose-mode: yes
|
||||
config-file: .mdlc.json
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,7 +4,6 @@ __pycache__/
|
||||
/build
|
||||
/dist
|
||||
*.egg-info/
|
||||
.pdm-python
|
||||
|
||||
# Manim files
|
||||
images/
|
||||
@ -45,6 +44,7 @@ paper/paper.pdf
|
||||
paper/media/
|
||||
|
||||
# Others
|
||||
.coverage*
|
||||
coverage.xml
|
||||
|
||||
rendering_times.csv
|
||||
|
8
.mdlc.json
Normal file
8
.mdlc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"replacementPatterns": [
|
||||
{
|
||||
"pattern": "^/(?<path>.*)$",
|
||||
"replacement": "https://eertmans.be/manim-slides/latest/$<path>.html"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,31 +1,47 @@
|
||||
ci:
|
||||
autofix_commit_msg: |
|
||||
chore(fmt): auto fixes from pre-commit.com hooks
|
||||
|
||||
for more information, see https://pre-commit.ci
|
||||
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.12.0
|
||||
rev: v2.14.0
|
||||
hooks:
|
||||
- id: pretty-format-yaml
|
||||
args: [--autofix]
|
||||
- id: pretty-format-toml
|
||||
exclude: poetry.lock
|
||||
args: [--autofix, --trailing-commas]
|
||||
- repo: https://github.com/keewis/blackdoc
|
||||
rev: v0.3.9
|
||||
hooks:
|
||||
- id: blackdoc
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.1
|
||||
rev: v0.6.8
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
rev: v1.11.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-setuptools]
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: github-issues
|
||||
name: GitHub issues link check
|
||||
description: Check issues (and PR) links are matching number.
|
||||
entry: python .github/scripts/check_github_issues.py
|
||||
language: system
|
||||
types: [markdown]
|
||||
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11.8
|
@ -19,5 +19,3 @@ python:
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- magic
|
||||
- sphinx-directive
|
||||
|
166
CHANGELOG.md
166
CHANGELOG.md
@ -8,7 +8,167 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
<!-- start changelog -->
|
||||
|
||||
(unreleased)=
|
||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.0...HEAD)
|
||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.8...HEAD)
|
||||
|
||||
(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 `manim-slides checkhealth` command to easily obtain important information
|
||||
for debug purposes.
|
||||
[#458](https://github.com/jeertmans/manim-slides/pull/458)
|
||||
- Added support for `disable_caching` and `flush_cache` options from Manim, and
|
||||
also the possibility to configure them through class options.
|
||||
[#452](https://github.com/jeertmans/manim-slides/pull/452)
|
||||
- Added `--to=zip` convert format to generate an archive with HTML output
|
||||
and asset files.
|
||||
[#470](https://github.com/jeertmans/manim-slides/pull/470)
|
||||
|
||||
(v5.1.8-chore)=
|
||||
### Chore
|
||||
|
||||
- Pin `rtoml==0.9.0` on Windows platforms,
|
||||
see [#398](https://github.com/jeertmans/manim-slides/pull/398),
|
||||
until
|
||||
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
|
||||
is solved.
|
||||
[#432](https://github.com/jeertmans/manim-slides/pull/432)
|
||||
- Removed an old validation check that prevented setting `loop=True` with
|
||||
`auto_next=True` on `next_slide()`
|
||||
[#445](https://github.com/jeertmans/manim-slides/pull/445)
|
||||
- Improved (and fixed) tests for Manim(GL), bumped minimal ManimCE version,
|
||||
improved coverage, and override dependency conflicts.
|
||||
[#447](https://github.com/jeertmans/manim-slides/pull/447)
|
||||
- Improved issue templates.
|
||||
[#456](https://github.com/jeertmans/manim-slides/pull/456)
|
||||
- Enhanced the error message when the slides folder does not exist.
|
||||
[#462](https://github.com/jeertmans/manim-slides/pull/462)
|
||||
- Fixed deprecation warnings.
|
||||
[#467](https://github.com/jeertmans/manim-slides/pull/467)
|
||||
- Documented potential fix for PPTX issue.
|
||||
[#475](https://github.com/jeertmans/manim-slides/pull/475)
|
||||
- Changed project manager from Rye to uv.
|
||||
[#476](https://github.com/jeertmans/manim-slides/pull/476)
|
||||
|
||||
(v5.1.8-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fix combining assets from multiple scenes to avoid filename collision.
|
||||
[#429](https://github.com/jeertmans/manim-slides/pull/429)
|
||||
- Fixed whitespace issue in default RevealJS template.
|
||||
[#442](https://github.com/jeertmans/manim-slides/pull/442)
|
||||
- Fixed black screen issue on recent Qt versions and device loss detected,
|
||||
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
|
||||
[#465](https://github.com/jeertmans/manim-slides/pull/465)
|
||||
|
||||
(v5.1.8-removed)=
|
||||
### Removed
|
||||
|
||||
- Removed `full-gl` extra, because it does not make sense to ship both
|
||||
`manimgl` and `manim` together.
|
||||
[#447](https://github.com/jeertmans/manim-slides/pull/447)
|
||||
|
||||
(v5.1.7)=
|
||||
## [v5.1.7](https://github.com/jeertmans/manim-slides/compare/v5.1.6...v5.1.7)
|
||||
|
||||
(v5.1.7-chore)=
|
||||
### Chore
|
||||
|
||||
- Improved the CI for bumping the version and README rendering on PyPI.
|
||||
[#425](https://github.com/jeertmans/manim-slides/pull/425)
|
||||
|
||||
(v5.1.6)=
|
||||
## [v5.1.6](https://github.com/jeertmans/manim-slides/compare/v5.1.5...v5.1.6)
|
||||
|
||||
(v5.1.6-added)=
|
||||
### Added
|
||||
|
||||
- Added options to skip the Manim Slides Sphinx directive.
|
||||
[#423](https://github.com/jeertmans/manim-slides/pull/423)
|
||||
|
||||
(v5.1.6-chore)=
|
||||
### Chore
|
||||
|
||||
- Added an examples gallery.
|
||||
[#422](https://github.com/jeertmans/manim-slides/pull/422)
|
||||
|
||||
(v5.1.5)=
|
||||
## [v5.1.5](https://github.com/jeertmans/manim-slides/compare/v5.1.4...v5.1.5)
|
||||
|
||||
(v5.1.5-chore)=
|
||||
### Chore
|
||||
|
||||
- Added CI for broken HTML links and fixed, plus spell checking.
|
||||
[#417](https://github.com/jeertmans/manim-slides/pull/417)
|
||||
- Create FAQ page and clear FAQ from README.md.
|
||||
[#418](https://github.com/jeertmans/manim-slides/pull/418)
|
||||
- Used Rye instead of PDM for faster development.
|
||||
[#420](https://github.com/jeertmans/manim-slides/pull/420)
|
||||
|
||||
(v5.1.5-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fixed broken `--show-config` command.
|
||||
[#419](https://github.com/jeertmans/manim-slides/pull/419)
|
||||
|
||||
(v5.1.4)=
|
||||
## [v5.1.4](https://github.com/jeertmans/manim-slides/compare/v5.1.3...v5.1.4)
|
||||
|
||||
(v5.1.4-added)=
|
||||
### Added
|
||||
|
||||
- Added audio output to `manim-slides present`.
|
||||
[#382](https://github.com/jeertmans/manim-slides/pull/382)
|
||||
|
||||
(v5.1.4-changed)=
|
||||
### Changed
|
||||
|
||||
- Added `--info-window-screen` option and change `--screen-number`
|
||||
to not move the info window.
|
||||
[#389](https://github.com/jeertmans/manim-slides/pull/389)
|
||||
|
||||
(v5.1.4-chore)=
|
||||
### Chore
|
||||
|
||||
- Created a favicon for the website/documentation.
|
||||
[#399](https://github.com/jeertmans/manim-slides/pull/399)
|
||||
- Documented the Nixpkg installation.
|
||||
[#404](https://github.com/jeertmans/manim-slides/pull/404 )
|
||||
- Updated the default RevealJS version to 5.1.0.
|
||||
[#412](https://github.com/jeertmans/manim-slides/pull/412)
|
||||
- Removed the `opencv-python` dependency.
|
||||
[#415](https://github.com/jeertmans/manim-slides/pull/415)
|
||||
|
||||
(v5.1.4-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fixed the retrieval of `background_color` with ManimCE.
|
||||
[#414](https://github.com/jeertmans/manim-slides/pull/414)
|
||||
- Fixed #390 issue caused by empty media created by ManimCE.
|
||||
[#416](https://github.com/jeertmans/manim-slides/pull/416)
|
||||
|
||||
(v5.1.3)=
|
||||
## [v5.1.3](https://github.com/jeertmans/manim-slides/compare/v5.1.2...v5.1.3)
|
||||
|
||||
(v5.1.3-chore)=
|
||||
### Chore
|
||||
|
||||
- Fix link in documentation.
|
||||
[#368](https://github.com/jeertmans/manim-slides/pull/368)
|
||||
|
||||
- Warn users if not using recommended Qt bindings.
|
||||
[#373](https://github.com/jeertmans/manim-slides/pull/373)
|
||||
|
||||
(v5.1.2)=
|
||||
## [v5.1.2](https://github.com/jeertmans/manim-slides/compare/v5.1.1...v5.1.2)
|
||||
|
||||
(v5.1.2-chore)=
|
||||
### Chore
|
||||
|
||||
- Fix ReadTheDocs version flyout in iframes.
|
||||
[#367](https://github.com/jeertmans/manim-slides/pull/367)
|
||||
|
||||
(v5.1.1)=
|
||||
## [v5.1.1](https://github.com/jeertmans/manim-slides/compare/v5.1.0...v5.1.1)
|
||||
@ -59,7 +219,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
(v5.1-chore)=
|
||||
### Chore
|
||||
|
||||
- Removed subrocess calls to FFMPEG with direct `libav` bindings using
|
||||
- Removed subrocess calls to FFmpeg with direct `libav` bindings using
|
||||
the `av` Python module. This should enhance rendering speed and security.
|
||||
[#335](https://github.com/jeertmans/manim-slides/pull/335)
|
||||
- Changed build backend to PDM and reflected on docs.
|
||||
@ -133,7 +293,7 @@ In an effort to better document changes, this CHANGELOG document is now created.
|
||||
but the new player should be much easier to maintain and more performant,
|
||||
than its predecessor.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Changed the slide config format to exclude unecessary information.
|
||||
- Changed the slide config format to exclude unnecessary information.
|
||||
`StypeType` is removed in favor to one boolean `loop` field. This is
|
||||
a **breaking change** and one should re-render the slides to apply changes.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
|
28
CITATION.cff
28
CITATION.cff
@ -3,25 +3,22 @@
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: Manim Slides
|
||||
message: A Python package for presenting Manim content anywhere
|
||||
message: >-
|
||||
If you use this software, please cite it using the
|
||||
metadata from this file.
|
||||
type: software
|
||||
authors:
|
||||
- name: Jérome Eertmans
|
||||
orcid: 'https://orcid.org/0000-0002-5579-5360'
|
||||
website: 'https://eertmans.be'
|
||||
identifiers:
|
||||
- type: doi
|
||||
value: 10.21105/jose.00206
|
||||
description: The paper presenting the software.
|
||||
repository-code: 'https://github.com/jeertmans/manim-slides'
|
||||
url: 'https://eertmans.be/manim-slides'
|
||||
abstract: >-
|
||||
Manim Slides is a Python package that makes presenting
|
||||
Manim animations straightforward. With minimal changes
|
||||
required to pre-existing code, one can slide through
|
||||
|
||||
animations in a PowerPoint-like manner, or share its
|
||||
slides online using ReavealJS’ power.
|
||||
slides online using ReavealJS' power.
|
||||
keywords:
|
||||
- Education
|
||||
- Math Animations
|
||||
@ -29,4 +26,19 @@ keywords:
|
||||
- PowerPoint
|
||||
- Python
|
||||
license: MIT
|
||||
version: v5.1.3
|
||||
version: v5.1.8
|
||||
preferred-citation:
|
||||
publisher:
|
||||
name: The Open Journal
|
||||
type: article
|
||||
authors:
|
||||
- name: Jérome Eertmans
|
||||
orcid: 'https://orcid.org/0000-0002-5579-5360'
|
||||
doi: 10.21105/jose.00206
|
||||
journal: Journal of Open Source Education
|
||||
month: 8
|
||||
year: 2023
|
||||
title: 'Manim Slides: A Python package for presenting Manim content anywhere'
|
||||
volume: 6
|
||||
number: 66
|
||||
pages: 206
|
||||
|
53
README.md
53
README.md
@ -4,6 +4,8 @@
|
||||
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
|
||||
</picture>
|
||||
|
||||
<!-- start pypi -->
|
||||
|
||||
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
||||
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
||||
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
|
||||
@ -28,8 +30,8 @@ Manim Slides will *automatically* detect the one you are using!
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Comparison with Similar Tools](#comparison-with-similar-tools)
|
||||
- [F.A.Q](#faq)
|
||||
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
|
||||
- [F.A.Q](https://eertmans.be/manim-slides/latest/faq.html)
|
||||
- [Citing](#citing)
|
||||
- [Contributing](#contributing)
|
||||
* [Reporting an Issue](#reporting-an-issue)
|
||||
* [Seeking for Help](#seeking-for-help)
|
||||
@ -49,7 +51,7 @@ for detailed install instructions.
|
||||
Using Manim Slides is a two-step process:
|
||||
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead
|
||||
of `Scene` (resp. `ThreeDScene`), and add calls to `self.next_slide()`
|
||||
everytime you want to create a new slide.
|
||||
every time you want to create a new slide.
|
||||
2. Run `manim-slides` on rendered animations and display them like a
|
||||
*PowerPoint* presentation.
|
||||
|
||||
@ -57,7 +59,7 @@ The documentation is available [online](https://eertmans.be/manim-slides/).
|
||||
|
||||
### Basic Example
|
||||
|
||||
Call `self.next_slide()` everytime you want to create a pause between
|
||||
Call `self.next_slide()` every time you want to create a pause between
|
||||
animations, and `self.next_slide(loop=True)` if you want the next slide to loop
|
||||
over animations until the user presses continue:
|
||||
|
||||
@ -118,7 +120,7 @@ manim-slides BasicExample
|
||||
</p>
|
||||
|
||||
For detailed usage documentation, run `manim-slides --help`, or go to the
|
||||
[documentation](https://eertmans.be/manim-slides/reference/cli.html).
|
||||
[documentation](https://eertmans.be/manim-slides/latest/reference/cli.html).
|
||||
|
||||
## Interactive Tutorial
|
||||
|
||||
@ -150,27 +152,28 @@ Below is a comparison of the most used ones with Manim Slides:
|
||||
| Web Browser presentations | Yes | No | Yes | No |
|
||||
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
|
||||
|
||||
## F.A.Q
|
||||
## Citing
|
||||
|
||||
### How to increase quality on Windows
|
||||
If you use this project, please cite it using the following reference:
|
||||
|
||||
On Windows platform, one may encounter a lower image resolution than expected.
|
||||
Usually, this is observed because Windows rescales every application to
|
||||
fit the screen.
|
||||
As found by [@arashash](https://github.com/arashash),
|
||||
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
|
||||
the problem can be addressed by changing the scaling factor to 100%:
|
||||
```bibtex
|
||||
@article{Jerome_Eertmans_Manim_Slides_A_2023,
|
||||
title = {{Manim Slides: A Python package for presenting Manim content anywhere}},
|
||||
author = {{Jérome Eertmans}},
|
||||
year = 2023,
|
||||
month = aug,
|
||||
journal = {Journal of Open Source Education},
|
||||
volume = 6,
|
||||
doi = {10.21105/jose.00206}
|
||||
}
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img alt="Windows Fix Scaling" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png">
|
||||
</p>
|
||||
|
||||
in *Settings*->*Display*.
|
||||
or by linking this GitHub repository at the end of the presentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are more than welcome! Please read through
|
||||
[our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
|
||||
[our contributing section](https://eertmans.be/manim-slides/latest/contributing/index.html).
|
||||
|
||||
### Reporting an Issue
|
||||
|
||||
@ -202,12 +205,10 @@ be able to help you!
|
||||
Sometimes, you may have a question about Manim Slides,
|
||||
not necessarily an issue.
|
||||
|
||||
There are two ways you can reach us for questions:
|
||||
|
||||
- via the `Question/Help/Support` topic when
|
||||
[choosing an issue template](https://github.com/jeertmans/manim-slides/issues/new/choose);
|
||||
- or via
|
||||
[GitHub discussions](https://github.com/jeertmans/manim-slides/discussions).
|
||||
First, make sure to read the
|
||||
[F.A.Q](https://eertmans.be/manim-slides/latest/faq.html) to see if
|
||||
your question has already been answered. If not, please follow the
|
||||
recommendation (from that page) to reach us for questions.
|
||||
|
||||
<!-- end seeking-for-help -->
|
||||
|
||||
@ -226,7 +227,7 @@ you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
|
||||
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/manim-slides
|
||||
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
|
||||
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
|
||||
[documentation-url]: https://manim-slides.readthedocs.io/en/latest/?badge=latest
|
||||
[documentation-url]: https://manim-slides.readthedocs.io/
|
||||
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
|
||||
[doi-url]: https://doi.org/10.5281/zenodo.8215167
|
||||
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
|
||||
|
1
docs/source/_static/favicon.png
Symbolic link
1
docs/source/_static/favicon.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/favicon.png
|
@ -32,7 +32,7 @@
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||
data-background-video-muted
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
{% if slide_config.loop -%}
|
||||
data-background-video-loop
|
||||
{%- endif -%}
|
||||
@ -261,7 +261,7 @@
|
||||
mouseWheel: {{ mouse_wheel }},
|
||||
|
||||
// Opens links in an iframe preview overlay
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
||||
// individually
|
||||
previewLinks: {{ preview_links }},
|
||||
|
||||
|
@ -55,6 +55,7 @@ add_module_names = False
|
||||
|
||||
html_theme = "furo"
|
||||
html_static_path = ["_static"]
|
||||
html_favicon = "_static/favicon.png"
|
||||
|
||||
html_theme_options = {
|
||||
"light_logo": "logo_light_transparent.png",
|
||||
|
@ -18,10 +18,10 @@ workflow
|
||||
internals
|
||||
```
|
||||
|
||||
[Workflow](./workflow)
|
||||
[Workflow](/contributing/workflow)
|
||||
: how to work on this project. Start here if you're a new contributor.
|
||||
|
||||
[Internals](./internals)
|
||||
[Internals](/contributing/internals)
|
||||
: how Manim Slides is built and how the various parts of it work.
|
||||
|
||||
## Reporting an Issue
|
||||
|
@ -18,65 +18,36 @@ Useful links:
|
||||
* [GitHub's Hello World](https://docs.github.com/en/get-started/quickstart/hello-world).
|
||||
* [GitHub Pull Request in 100 Seconds](https://www.youtube.com/watch?v=8lGpZkjnkt4&ab_channel=Fireship).
|
||||
|
||||
Once you feel comfortable with git and GitHub, [fork](https://github.com/jeertmans/manim-slides/fork) the repository, and clone it locally.
|
||||
Once you feel comfortable with git and GitHub,
|
||||
[fork](https://github.com/jeertmans/manim-slides/fork)
|
||||
the repository, and clone it locally.
|
||||
|
||||
As for every Python project, using virtual environment is recommended to avoid
|
||||
conflicts between modules.
|
||||
For this project, we use [PDM](https://pdm-project.org/) to easily manage project
|
||||
For this project, we use [uv](https://github.com/astral-sh/uv) to easily manage project
|
||||
and development dependencies. If not already, please install this tool.
|
||||
|
||||
## Installing Python modules
|
||||
|
||||
With PDM, installation becomes straightforward:
|
||||
With uv, installation becomes straightforward:
|
||||
|
||||
```bash
|
||||
pdm install
|
||||
```
|
||||
|
||||
This, however, only installs the minimal set of dependencies to run the package.
|
||||
|
||||
If you would like to install Manim or ManimGL,
|
||||
as documented in the [quickstart](../quickstart),
|
||||
you can use the `-G|--group` option:
|
||||
|
||||
```bash
|
||||
pdm install -Gmanim # For Manim
|
||||
# or
|
||||
pdm install -Gmanimgl # For ManimGL
|
||||
```
|
||||
|
||||
Additionnally, Manim Slides comes with groups of dependencies for development purposes:
|
||||
|
||||
```bash
|
||||
pdm install -Gdev # For linters and formatters
|
||||
# or
|
||||
pdm install -Gdocs # To build the documentation locally
|
||||
# or
|
||||
pdm install -Gtest # To run tests
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
:::{note}
|
||||
You can combine any number of groups or extras when installing the package locally.
|
||||
|
||||
You can also install everything with `pdm install -G:all`.
|
||||
You still need the same dependencies as to install Manim and ManimGL,
|
||||
so please check their respective installation guides.
|
||||
:::
|
||||
|
||||
## Running commands
|
||||
|
||||
Because modules are installed in a new Python environment,
|
||||
you cannot use them directly in the shell.
|
||||
Instead, you either need to prepend `pdm run` to any command, e.g.:
|
||||
Instead, you either need to prepend `uv run` to any command, e.g.:
|
||||
|
||||
```bash
|
||||
pdm run manim-slides wizard
|
||||
```
|
||||
|
||||
or [enter a new shell](https://pdm-project.org/latest/usage/venv/#activate-a-virtualenv)
|
||||
that uses this new Python environment:
|
||||
|
||||
```bash
|
||||
eval $(pdm venv activate) # Click on the link above to see shell-specific command
|
||||
manim-slides wizard
|
||||
uv run manim-slides wizard
|
||||
```
|
||||
|
||||
## Testing your code
|
||||
@ -85,7 +56,7 @@ Most of the tests are done with GitHub actions, thus not on your computer.
|
||||
The only command you should run locally is:
|
||||
|
||||
```bash
|
||||
pdm run pre-commit run --all-files
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
|
||||
This runs a few linter and formatter to make sure the code quality and style stay
|
||||
@ -95,7 +66,7 @@ If a warning or an error is displayed, please fix it before going to next step.
|
||||
For testing your code, simply run:
|
||||
|
||||
```bash
|
||||
pdm run pytest
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## Building the documentation
|
||||
@ -107,7 +78,7 @@ To generate the documentation, run the following:
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
pdm run make html
|
||||
uv run make html
|
||||
```
|
||||
|
||||
Then, the output index file is located at `docs/build/html/index.html` and
|
||||
|
129
docs/source/faq.md
Normal file
129
docs/source/faq.md
Normal file
@ -0,0 +1,129 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
This page summarizes a few of the most frequently asked questions
|
||||
when using Manim Slides.
|
||||
|
||||
They are organized by topic.
|
||||
|
||||
If your question is not here, please first look through the
|
||||
[open **and closed** issues on GitHub](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue)
|
||||
or within the [discussions](https://github.com/jeertmans/manim-slides/discussions).
|
||||
|
||||
If you still cannot find help after that, do not hesitate to create
|
||||
your own issue or discussion on GitHub!
|
||||
|
||||
## Installing
|
||||
|
||||
Everything related to installing Manim-Slides.
|
||||
|
||||
Please do not forget the carefully read through
|
||||
the [installation](/installation) page!
|
||||
|
||||
## Rendering
|
||||
|
||||
Questions related to `manim-slides render [SCENES]...`,
|
||||
|
||||
### I cannot render with ManimGL
|
||||
|
||||
ManimGL support is only guaranteed to work
|
||||
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
||||
and its development is not very active.
|
||||
|
||||
The typical issues are that (1) ManimGL needs an outdated NumPy version
|
||||
and (2) ManimGL **should not** be installed from the GitHub repository,
|
||||
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
|
||||
|
||||
Questions related to `manim-slides present [SCENES]...`,
|
||||
or `manim-slides [SCENES]...` for short.
|
||||
|
||||
### Can I have interactive slides
|
||||
|
||||
No. Slides are pre-rendered static videos files
|
||||
and cannot be modified on the fly.
|
||||
|
||||
If you need new to have some kind of interactive, look
|
||||
at the preview feature coupled with the OpenGL renderer
|
||||
with ManimCE or ManimGL.
|
||||
|
||||
### Slides go black when video finishes
|
||||
|
||||
This is an issue with Qt,
|
||||
which cannot be solved on all platforms and Python versions,
|
||||
see [#293](https://github.com/jeertmans/manim-slides/issues/293).
|
||||
|
||||
Recent version of Manim Slides, i.e., `manim-slides>5.1.7`, come
|
||||
with a fix that should work fine.
|
||||
|
||||
### How to increase quality on Windows
|
||||
|
||||
On Windows platform, one may encounter a lower image resolution than expected.
|
||||
Usually, this is observed because Windows rescales every application to
|
||||
fit the screen.
|
||||
As found by [@arashash](https://github.com/arashash),
|
||||
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
|
||||
the problem can be addressed by changing the scaling factor to 100%:
|
||||
|
||||
<p align="center">
|
||||
<img
|
||||
alt="Windows Fix Scaling"
|
||||
src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png"
|
||||
>
|
||||
</p>
|
||||
|
||||
in *Settings*->*Display*.
|
||||
|
||||
## Converting to any format
|
||||
|
||||
Questions that apply to all output formats when using
|
||||
`manim-slides convert [SCENES]...`.
|
||||
|
||||
### What are all possible configuration options
|
||||
|
||||
Configuration options can be specified with the syntax
|
||||
`-c<option_name>=<option_value>`.
|
||||
|
||||
To list all accepted options, use `manim-slides convert --to=FORMAT --show-config`,
|
||||
where `FORMAT` is one of the supported formats.
|
||||
This will also show the default value for each option.
|
||||
|
||||
### How to retrieve the current template
|
||||
|
||||
If you want to create your own template, the best is to start from the default one.
|
||||
|
||||
You can either download it from the
|
||||
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/templates)
|
||||
or use the `manim-slides convert --to=FORMAT --show-template` command,
|
||||
where `FORMAT` is one of the supported formats.
|
||||
|
||||
## Converting to HTML
|
||||
|
||||
Questions related to `manim-slides convert [SCENES]... output.html`.
|
||||
|
||||
### I moved my `.html` file and it stopped working
|
||||
|
||||
If you did not specify `-cdata_uri=true` when converting,
|
||||
then Manim Slides generated a folder containing all
|
||||
the video files, in the same folder as the HTML
|
||||
output. As the path to video files is a relative path,
|
||||
you need to move the HTML **and its assets** altogether.
|
||||
|
||||
## Converting to PPTX
|
||||
|
||||
Questions related to `manim-slides convert [SCENES]... output.pptx`.
|
||||
|
||||
### My media stop playing after a few slides
|
||||
|
||||
This issue is (probably) caused by PowerPoint never freeing
|
||||
memory, causing memory allocation errors, and can be partially
|
||||
solved by reducing the video quality or the number of slides,
|
||||
see [#392](https://github.com/jeertmans/manim-slides/issues/392).
|
||||
|
||||
Another solution, suggested by [@Azercoco](https://github.com/Azercoco) in
|
||||
[#392 (comment)](https://github.com/jeertmans/manim-slides/issues/392#issuecomment-2368198106),
|
||||
is to disable hardware/GPU acceleration.
|
75
docs/source/gallery.md
Normal file
75
docs/source/gallery.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Examples Gallery
|
||||
|
||||
With Manim, the only limit to what you can create is your imagination!
|
||||
*This also applies to Manim Slides.*
|
||||
|
||||
As the field of possibilities is infinitely vast,
|
||||
it's often useful to **learn** how to use Manim Slides **based on examples**.
|
||||
|
||||
The aim of this page is to share with you the creations of some
|
||||
Manim Slides users, to hopefully inspire you!
|
||||
Most of them use HTML conversion to make them accessible via a website.
|
||||
|
||||
If you too have created content with Manim Slides that is available online
|
||||
(e.g., a YouTube video or website),
|
||||
don't hesitate to contact us so that we can share your content on this page!
|
||||
|
||||
## Scientif Research
|
||||
|
||||
Below are people that dissimenate their research results
|
||||
using Manim Slides presentations.
|
||||
|
||||
### Daniel Panizo Pérez
|
||||
|
||||
Daniel publishes his presentations on *Cosmology, String Theory and related*
|
||||
topics on his
|
||||
[personal website](https://panopepino.github.io/Web_Page/main_page/slides.html). https://panopepino.github.io/Web_Page/main_page/slides.html
|
||||
|
||||
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).
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;">
|
||||
<iframe
|
||||
loading="lazy"
|
||||
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||
frameborder="1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allowfullscreen
|
||||
allow="autoplay"
|
||||
src="https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
He also shares his code on a public
|
||||
[GitHub repository](https://github.com/PanoPepino/Manim_Theoretical).
|
||||
|
||||
### Jérome Eertmans
|
||||
|
||||
Jérome, the author of Manim Slides, publishes his presentations
|
||||
on the topic of *Ray Tracing applied to Radio Propagations* on his
|
||||
[personal website](https://eertmans.be). He also uses Manim Slides
|
||||
for presenting at conferences using the *PowerPoint* or HTML conversion.
|
||||
|
||||
For example, below are the slides of his
|
||||
[PhD confirmation](https://eertmans.be/posts/confirmation2023-presentation/).
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;">
|
||||
<iframe
|
||||
loading="lazy"
|
||||
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||
frameborder="1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allowfullscreen
|
||||
allow="autoplay"
|
||||
src="https://eertmans.be/assets/slides/2023-12-07-confirmation.html">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
## School Work
|
||||
|
||||
Below are people that used Manim Slides for school presentations.
|
||||
|
||||
*This list is currently empty. Please reach out to us if you have examples
|
||||
to share!*
|
@ -23,7 +23,7 @@ og:description: Manim Slides makes creating slides with Manim super easy!
|
||||
|
||||
Manim Slides makes creating slides with Manim super easy!
|
||||
|
||||
In a [very few steps](./quickstart),
|
||||
In a [very few steps](/quickstart),
|
||||
you can create slides and present them either using the GUI, or your browser.
|
||||
|
||||
Slide through the demo below to get a quick glimpse on what you can do with
|
||||
@ -43,6 +43,8 @@ installation
|
||||
reference/index
|
||||
features_table
|
||||
manim_or_manimgl
|
||||
gallery
|
||||
faq
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
|
@ -12,7 +12,7 @@ The benefit of using pipx is that it will automatically create a new virtual
|
||||
environment for every package you install.
|
||||
|
||||
:::{note}
|
||||
Everytime you read `pipx install`, you can use `pip install` instead,
|
||||
Every time you read `pipx install`, you can use `pip install` instead,
|
||||
if you are working in a virtual environment or else.
|
||||
:::
|
||||
|
||||
@ -106,17 +106,14 @@ Along with the optional dependencies for Manim and ManimGL,
|
||||
Manim Slides offers additional *extras*, that can be activated
|
||||
using optional dependencies:
|
||||
|
||||
- `full`, to include `magic`, `manim`, `manimgl`, and
|
||||
- `full`, to include `magic`, `manim`, and
|
||||
`sphinx-directive` extras (see below);
|
||||
- `magic`, to include a Jupyter magic to render
|
||||
animations inside notebooks. This automatically installs `manim`,
|
||||
and does not work with ManimGL;
|
||||
- `manim` and `manimgl`, for installing the corresponding
|
||||
dependencies;
|
||||
- `pyqt6` to include PyQt6 Qt bindings. Those bindings are available
|
||||
on most platforms and Python version, but produce a weird black
|
||||
screen between slide with `manim-slides present`,
|
||||
see [#QTBUG-118501](https://bugreports.qt.io/browse/QTBUG-118501);
|
||||
- `pyqt6` to include PyQt6 Qt bindings;
|
||||
- `pyqt6-full` to include `full` and `pyqt6`;
|
||||
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
|
||||
on most platforms and Python version, except on Python 3.12[^2];
|
||||
@ -134,6 +131,38 @@ pipx install -U "manim-slides[extra1,extra2]"
|
||||
[^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
|
||||
observe the same visual bug as with PyQt6.
|
||||
|
||||
## Nixpkgs installation
|
||||
|
||||
Manim Slides is distributed under Nixpkgs >=24.05.
|
||||
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
|
||||
includes pyqt6 (see above);
|
||||
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
||||
module (for notebook magics), and includes IPython but not does not include
|
||||
any Qt bindings.
|
||||
|
||||
You can try out the Manim Slides package with
|
||||
|
||||
```sh
|
||||
nix-shell -p manim ffmpeg manim-slides
|
||||
```
|
||||
|
||||
or by adding it to your
|
||||
[configuration file](https://nixos.org/manual/nixos/stable/#sec-package-management).
|
||||
|
||||
Alternatively, you can try Manim Slides in a Python environment with:
|
||||
|
||||
```sh
|
||||
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).
|
||||
|
||||
:::{note}
|
||||
Nix current does not support `manimgl`.
|
||||
:::
|
||||
|
||||
## When you need a Qt backend
|
||||
|
||||
Before `v5.1`, Manim Slides automatically included PySide6 as
|
||||
@ -153,5 +182,5 @@ install those are via optional dependencies, as explained above.
|
||||
|
||||
An alternative way to install Manim Slides is to clone the git repository,
|
||||
and build the package from source. Read the
|
||||
[contributing guide](./contributing/workflow)
|
||||
[contributing guide](/contributing/workflow)
|
||||
to know how to process.
|
||||
|
@ -15,7 +15,6 @@ the slides.
|
||||
If both modules are present in {py:data}`sys.modules`, then Manim Slides will
|
||||
prefer using `manim`.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
The simplest way to use Manim Slides with the correct Manim API is to:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Quickstart
|
||||
|
||||
If not already, install Manim Slides, along with either Manim or ManimGL,
|
||||
see [installation](./installation).
|
||||
see [installation](/installation).
|
||||
|
||||
## Creating your first slides
|
||||
|
||||
@ -31,4 +31,5 @@ The output slides should look this this:
|
||||
:quality: high
|
||||
```
|
||||
|
||||
For more advanced examples, see the [Examples](reference/examples) section.
|
||||
For more advanced examples,
|
||||
see the [Examples](/reference/examples) section.
|
||||
|
@ -2,7 +2,8 @@
|
||||
|
||||
Manim Slides' graphical user interface (GUI) is the *de facto* way to present slides.
|
||||
|
||||
If you do not specify one of the commands listed in the [CLI reference](./cli),
|
||||
If you do not specify one of the commands listed in the
|
||||
[CLI reference](/reference/cli),
|
||||
Manim Slides will use **present** by default, which launches a GUI window,
|
||||
playing your scene(s) like so:
|
||||
|
||||
@ -25,7 +26,7 @@ directory, you should not worry about that :-)
|
||||
## Configuration File
|
||||
|
||||
It is possible to configure Manim Slides via a configuration file, even though
|
||||
this feature is currently limited. You may initiliaze the default configuration
|
||||
this feature is currently limited. You may initialize the default configuration
|
||||
file with:
|
||||
|
||||
```bash
|
||||
|
@ -30,11 +30,11 @@ manim-slides convert --show-config
|
||||
## Using a Custom Template
|
||||
|
||||
The default template used for HTML conversion can be found on
|
||||
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/data/revealjs_template.html)
|
||||
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html)
|
||||
or printed with the `--show-template` option.
|
||||
If you wish to use another template, you can do so with the
|
||||
`--use-template FILE` option.
|
||||
|
||||
## More about HTML Slides
|
||||
|
||||
You can read more about HTML slides in the [sharing](./sharing) section.
|
||||
You can read more about HTML slides in the [sharing](/reference/sharing) section.
|
||||
|
@ -16,23 +16,23 @@ sharing
|
||||
Sphinx Extension <sphinx_extension>
|
||||
```
|
||||
|
||||
[Application Programming Interface](./api): list of classes and methods that may
|
||||
[Application Programming Interface](/reference/api): list of classes and methods that may
|
||||
be useful to the end-user.
|
||||
|
||||
[Command Line Interface](./cli): list of all commands available using Manim
|
||||
[Command Line Interface](/reference/cli): list of all commands available using Manim
|
||||
Slides' executable.
|
||||
|
||||
[Examples](./examples): curated list of examples and their output.
|
||||
[Examples](/reference/examples): curated list of examples and their output.
|
||||
|
||||
[Graphical User Interface](./gui): details about the main Manim Slide' feature.
|
||||
[Graphical User Interface](/reference/gui): details about the main Manim Slide' feature.
|
||||
|
||||
[HTML Presentation](./html): an alternative way of presenting your animations.
|
||||
[HTML Presentation](/reference/html): an alternative way of presenting your animations.
|
||||
|
||||
[IPython Magic](./ipython_magic): a magic to render and display Manim Slides inside notebooks.
|
||||
[IPython Magic](/reference/ipython_magic): a magic to render and display Manim Slides inside notebooks.
|
||||
|
||||
+ [Example](./magic_example): example notebook using the magics.
|
||||
+ [Example](/reference/magic_example): example notebook using the magics.
|
||||
|
||||
[Sharing](./sharing): how to share your presentation with others.
|
||||
[Sharing](/reference/sharing): how to share your presentation with others.
|
||||
|
||||
|
||||
[Sphinx Extension](./sphinx_extension): a Sphinx extension for diplaying Manim Slides animations within your documentation.
|
||||
[Sphinx Extension](/reference/sphinx_extension): a Sphinx extension for displaying Manim Slides animations within your documentation.
|
||||
|
@ -18,6 +18,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from manim import *\n",
|
||||
"\n",
|
||||
"from manim_slides import *"
|
||||
]
|
||||
},
|
||||
|
@ -126,10 +126,15 @@ to use an `iframe`:
|
||||
</div>
|
||||
```
|
||||
|
||||
<!-- markdown-link-check-disable -->
|
||||
<!-- see why: https://github.com/tcort/markdown-link-check/discussions/189 -->
|
||||
|
||||
The additional code comes from
|
||||
[this article](https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page)
|
||||
and it there to preserve the original aspect ratio (16:9).
|
||||
|
||||
<!-- markdown-link-check-enable -->
|
||||
|
||||
### Sharing ONE HTML file
|
||||
|
||||
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
|
||||
@ -142,8 +147,8 @@ HTML conversion makes it convenient to play your presentation on a
|
||||
remote server.
|
||||
|
||||
This is how your are able to watch all the examples on this website. If you want
|
||||
to know how to share your slide with GitHub pages, see the
|
||||
[workflow file](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/pages.yml).
|
||||
to know how to share your slide with GitHub pages, check out the
|
||||
[Manim Slides Starter GitHub repository template](https://github.com/jeertmans/manim-slides-starter).
|
||||
|
||||
:::{warning}
|
||||
Keep in mind that playing large video files over the internet network
|
||||
@ -161,7 +166,7 @@ Pages. Please refer to the template page for usage instructions.
|
||||
|
||||
### With PowerPoint (*EXPERIMENTAL*)
|
||||
|
||||
A recent conversion feature is to the PowerPoint format, thanks to the
|
||||
A convenient conversion feature is to the PowerPoint format, thanks to the
|
||||
`python-pptx` package. Even though it is fully working,
|
||||
it is still considered in an *EXPERIMENTAL* status because we do not
|
||||
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
|
||||
|
@ -40,7 +40,7 @@ class ConvertExample(Slide):
|
||||
step_2 = Text("2. Replace Scene with Slide")
|
||||
step_3 = Text("3. In construct, add pauses where you need")
|
||||
step_4 = Text("4. You can also create loops")
|
||||
step_5 = Text("5. Render you scene with Manim")
|
||||
step_5 = Text("5. Render your scene with Manim")
|
||||
step_6 = Text("6. Open your presentation with Manim Slides")
|
||||
|
||||
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
|
||||
@ -148,7 +148,7 @@ class Example(Slide):
|
||||
)
|
||||
|
||||
code_step_5 = Code(
|
||||
code="manim example.py Example",
|
||||
code="manim-slide render example.py Example",
|
||||
language="console",
|
||||
)
|
||||
|
||||
@ -191,7 +191,7 @@ class Example(Slide):
|
||||
self.play(Transform(code, code_step_6))
|
||||
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
|
||||
|
||||
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
|
||||
watch_text = Text("Watch results on next slides!").shift(2 * DOWN).scale(0.5)
|
||||
|
||||
self.next_slide(loop=True)
|
||||
self.play(FadeIn(watch_text))
|
||||
|
@ -1,6 +1,6 @@
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any, List
|
||||
from typing import Any
|
||||
|
||||
from .__version__ import __version__
|
||||
|
||||
@ -29,7 +29,7 @@ class Module(ModuleType):
|
||||
|
||||
return ModuleType.__getattribute__(self, name)
|
||||
|
||||
def __dir__(self) -> List[str]:
|
||||
def __dir__(self) -> list[str]:
|
||||
result = list(new_module.__all__)
|
||||
result.extend(
|
||||
(
|
||||
|
@ -5,6 +5,7 @@ import requests
|
||||
from click_default_group import DefaultGroup
|
||||
|
||||
from .__version__ import __version__
|
||||
from .checkhealth import checkhealth
|
||||
from .convert import convert
|
||||
from .logger import logger
|
||||
from .present import list_scenes, present
|
||||
@ -63,6 +64,7 @@ def cli(notify_outdated_version: bool) -> None:
|
||||
|
||||
|
||||
cli.add_command(convert)
|
||||
cli.add_command(checkhealth)
|
||||
cli.add_command(init)
|
||||
cli.add_command(list_scenes)
|
||||
cli.add_command(present)
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "5.1.3"
|
||||
__version__ = "5.1.8"
|
||||
|
37
manim_slides/checkhealth.py
Normal file
37
manim_slides/checkhealth.py
Normal file
@ -0,0 +1,37 @@
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
from .__version__ import __version__
|
||||
|
||||
|
||||
@click.command()
|
||||
def checkhealth() -> None:
|
||||
"""Check Manim Slides' installation."""
|
||||
click.echo(f"Manim Slides version: {__version__}")
|
||||
click.echo(f"Python executable: {sys.executable}")
|
||||
click.echo("Manim bindings:")
|
||||
|
||||
try:
|
||||
from manim import __version__ as manimce_version
|
||||
|
||||
click.echo(f"\tmanim (version: {manimce_version})")
|
||||
except ImportError:
|
||||
click.secho("\tmanim not found", bold=True)
|
||||
|
||||
try:
|
||||
from manimlib import __version__ as manimlib_version
|
||||
|
||||
click.echo(f"\tmanimgl (version: {manimlib_version})")
|
||||
except ImportError:
|
||||
click.secho("\tmanimgl not found", bold=True)
|
||||
|
||||
try:
|
||||
from qtpy import API, QT_VERSION
|
||||
|
||||
click.echo(f"Qt API: {API} (version: {QT_VERSION})")
|
||||
except ImportError:
|
||||
click.secho(
|
||||
"No Qt API found, some Manim Slides commands will not be available",
|
||||
bold=True,
|
||||
)
|
@ -70,11 +70,22 @@ def verbosity_option(function: F) -> F:
|
||||
|
||||
def folder_path_option(function: F) -> F:
|
||||
"""Wrap a function to add folder path option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: Path) -> Path:
|
||||
if not value.exists():
|
||||
raise click.UsageError(
|
||||
f"Invalid value for '--folder': Directory '{value}' does not exist. "
|
||||
"Did you render the animations first?",
|
||||
ctx=ctx,
|
||||
)
|
||||
return value
|
||||
|
||||
wrapper: Wrapper = click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
type=click.Path(file_okay=False, path_type=Path),
|
||||
callback=callback,
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)
|
||||
|
@ -4,7 +4,7 @@ from functools import wraps
|
||||
from inspect import Parameter, signature
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import rtoml
|
||||
from pydantic import (
|
||||
@ -13,6 +13,8 @@ from pydantic import (
|
||||
FilePath,
|
||||
PositiveInt,
|
||||
PrivateAttr,
|
||||
conset,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
@ -24,7 +26,7 @@ Receiver = Callable[..., Any]
|
||||
|
||||
|
||||
class Signal(BaseModel): # type: ignore[misc]
|
||||
__receivers: List[Receiver] = PrivateAttr(default_factory=list)
|
||||
__receivers: list[Receiver] = PrivateAttr(default_factory=list)
|
||||
|
||||
def connect(self, receiver: Receiver) -> None:
|
||||
self.__receivers.append(receiver)
|
||||
@ -47,20 +49,13 @@ def key_id(name: str) -> PositiveInt:
|
||||
class Key(BaseModel): # type: ignore[misc]
|
||||
"""Represents a list of key codes, with optionally a name."""
|
||||
|
||||
ids: List[PositiveInt] = Field(unique=True)
|
||||
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
|
||||
name: Optional[str] = None
|
||||
|
||||
__signal: Signal = PrivateAttr(default_factory=Signal)
|
||||
|
||||
@field_validator("ids")
|
||||
@classmethod
|
||||
def ids_is_non_empty_set(cls, ids: Set[Any]) -> Set[Any]:
|
||||
if len(ids) <= 0:
|
||||
raise ValueError("Key's ids must be a non-empty set")
|
||||
return ids
|
||||
|
||||
def set_ids(self, *ids: int) -> None:
|
||||
self.ids = list(set(ids))
|
||||
self.ids = set(ids)
|
||||
|
||||
def match(self, key_id: int) -> bool:
|
||||
m = key_id in self.ids
|
||||
@ -77,6 +72,10 @@ class Key(BaseModel): # type: ignore[misc]
|
||||
def connect(self, function: Receiver) -> None:
|
||||
self.__signal.connect(function)
|
||||
|
||||
@field_serializer("ids")
|
||||
def serialize_dt(self, ids: set[int]) -> list[int]:
|
||||
return list(self.ids)
|
||||
|
||||
|
||||
class Keys(BaseModel): # type: ignore[misc]
|
||||
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
|
||||
@ -98,8 +97,8 @@ class Keys(BaseModel): # type: ignore[misc]
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
|
||||
ids: Set[int] = set()
|
||||
def ids_are_unique_across_keys(cls, values: dict[str, Key]) -> dict[str, Key]:
|
||||
ids: set[int] = set()
|
||||
|
||||
for key in values.values():
|
||||
if len(ids.intersection(key["ids"])) != 0:
|
||||
@ -180,7 +179,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
||||
fun_kwargs = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in cls.__fields__
|
||||
if key not in cls.model_fields
|
||||
}
|
||||
fun_kwargs[arg_name] = cls(**kwargs)
|
||||
return fun(*args, **fun_kwargs)
|
||||
@ -194,7 +193,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
||||
default=field_info.default,
|
||||
annotation=field_info.annotation,
|
||||
)
|
||||
for field_name, field_info in cls.__fields__.items()
|
||||
for field_name, field_info in cls.model_fields.items()
|
||||
]
|
||||
|
||||
sig = sig.replace(parameters=parameters)
|
||||
@ -231,7 +230,7 @@ class PreSlideConfig(BaseSlideConfig):
|
||||
return cls(
|
||||
start_animation=start_animation,
|
||||
end_animation=end_animation,
|
||||
**base_slide_config.dict(),
|
||||
**base_slide_config.model_dump(),
|
||||
)
|
||||
|
||||
@field_validator("start_animation", "end_animation")
|
||||
@ -249,7 +248,11 @@ class PreSlideConfig(BaseSlideConfig):
|
||||
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
|
||||
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
|
||||
raise ValueError(
|
||||
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
|
||||
"You have to play at least one animation (e.g., `self.wait()`) "
|
||||
"before pausing. If you want to start paused, use the appropriate "
|
||||
"command-line option when presenting. "
|
||||
"IMPORTANT: when using ManimGL, `self.wait()` is not considered "
|
||||
"to be an animation, so prefer to directly use `self.play(...)`."
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
@ -258,21 +261,6 @@ class PreSlideConfig(BaseSlideConfig):
|
||||
|
||||
return pre_slide_config
|
||||
|
||||
@model_validator(mode="after")
|
||||
@classmethod
|
||||
def loop_and_auto_next_disallowed(
|
||||
cls, pre_slide_config: "PreSlideConfig"
|
||||
) -> "PreSlideConfig":
|
||||
if pre_slide_config.loop and pre_slide_config.auto_next:
|
||||
raise ValueError(
|
||||
"You cannot have both `loop=True` and `auto_next=True`, "
|
||||
"because a looping slide has no ending. "
|
||||
"This may be supported in the future if "
|
||||
"https://github.com/jeertmans/manim-slides/pull/299 gets merged."
|
||||
)
|
||||
|
||||
return pre_slide_config
|
||||
|
||||
@property
|
||||
def slides_slice(self) -> slice:
|
||||
return slice(self.start_animation, self.end_animation)
|
||||
@ -288,12 +276,12 @@ class SlideConfig(BaseSlideConfig):
|
||||
def from_pre_slide_config_and_files(
|
||||
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
||||
) -> "SlideConfig":
|
||||
return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
|
||||
return cls(file=file, rev_file=rev_file, **pre_slide_config.model_dump())
|
||||
|
||||
|
||||
class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||
slides: List[SlideConfig] = Field(min_length=1)
|
||||
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
|
||||
slides: list[SlideConfig] = Field(min_length=1)
|
||||
resolution: tuple[PositiveInt, PositiveInt] = (1920, 1080)
|
||||
background_color: Color = "black"
|
||||
|
||||
@classmethod
|
||||
@ -320,23 +308,22 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||
f.write(self.model_dump_json(indent=2))
|
||||
|
||||
def copy_to(
|
||||
self, folder: Path, use_cached: bool = True, include_reversed: bool = True
|
||||
) -> "PresentationConfig":
|
||||
self,
|
||||
folder: Path,
|
||||
use_cached: bool = True,
|
||||
include_reversed: bool = True,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
"""Copy the files to a given directory."""
|
||||
for slide_config in self.slides:
|
||||
file = slide_config.file
|
||||
rev_file = slide_config.rev_file
|
||||
|
||||
dest = folder / file.name
|
||||
rev_dest = folder / rev_file.name
|
||||
|
||||
slide_config.file = dest
|
||||
slide_config.rev_file = rev_dest
|
||||
dest = folder / f"{prefix}{file.name}"
|
||||
rev_dest = folder / f"{prefix}{rev_file.name}"
|
||||
|
||||
if not use_cached or not dest.exists():
|
||||
shutil.copy(file, dest)
|
||||
|
||||
if include_reversed and (not use_cached or not rev_dest.exists()):
|
||||
shutil.copy(rev_file, rev_dest)
|
||||
|
||||
return self
|
||||
|
@ -1,18 +1,19 @@
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import webbrowser
|
||||
from base64 import b64encode
|
||||
from collections import deque
|
||||
from enum import Enum
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
import av
|
||||
import click
|
||||
import cv2
|
||||
import pptx
|
||||
from click import Context, Parameter
|
||||
from jinja2 import Template
|
||||
@ -52,7 +53,7 @@ def open_with_default(file: Path) -> None:
|
||||
|
||||
def validate_config_option(
|
||||
ctx: Context, param: Parameter, value: Any
|
||||
) -> Dict[str, str]:
|
||||
) -> dict[str, str]:
|
||||
config = {}
|
||||
|
||||
for c_option in value:
|
||||
@ -79,11 +80,23 @@ def file_to_data_uri(file: Path) -> str:
|
||||
|
||||
def get_duration_ms(file: Path) -> float:
|
||||
"""Read a video and return its duration in milliseconds."""
|
||||
cap = cv2.VideoCapture(str(file))
|
||||
fps: int = cap.get(cv2.CAP_PROP_FPS)
|
||||
frame_count: int = cap.get(cv2.CAP_PROP_FRAME_COUNT)
|
||||
with av.open(str(file)) as container:
|
||||
video = container.streams.video[0]
|
||||
|
||||
return 1000 * frame_count / fps
|
||||
return float(1000 * video.duration * video.time_base)
|
||||
|
||||
|
||||
def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
|
||||
"""Read a image from a video file at a given index."""
|
||||
with av.open(str(file)) as container:
|
||||
frames = container.decode(video=0)
|
||||
|
||||
if frame_index == FrameIndex.last:
|
||||
(frame,) = deque(frames, 1)
|
||||
else:
|
||||
frame = next(frames)
|
||||
|
||||
return frame.to_image()
|
||||
|
||||
|
||||
class Converter(BaseModel): # type: ignore
|
||||
@ -103,17 +116,18 @@ class Converter(BaseModel): # type: ignore
|
||||
"""
|
||||
return ""
|
||||
|
||||
def open(self, file: Path) -> Any:
|
||||
def open(self, file: Path) -> None:
|
||||
"""Open a file, generated with converter, using appropriate application."""
|
||||
raise NotImplementedError
|
||||
open_with_default(file)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s: str) -> Type["Converter"]:
|
||||
def from_string(cls, s: str) -> type["Converter"]:
|
||||
"""Return the appropriate converter from a string name."""
|
||||
return {
|
||||
"html": RevealJS,
|
||||
"pdf": PDF,
|
||||
"pptx": PowerPoint,
|
||||
"zip": HtmlZip,
|
||||
}[s]
|
||||
|
||||
|
||||
@ -316,7 +330,7 @@ class RevealJS(Converter):
|
||||
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
|
||||
auto_animate_duration: float = 1.0
|
||||
auto_animate_unmatched: JsBool = JsBool.true
|
||||
auto_animate_styles: List[str] = Field(
|
||||
auto_animate_styles: list[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"opacity",
|
||||
"color",
|
||||
@ -355,7 +369,7 @@ class RevealJS(Converter):
|
||||
hide_cursor_time: int = 5000
|
||||
# Appearance options from RevealJS
|
||||
background_color: Color = "black"
|
||||
reveal_version: str = "4.6.1"
|
||||
reveal_version: str = "5.1.0"
|
||||
reveal_theme: RevealTheme = RevealTheme.black
|
||||
title: str = "Manim Slides"
|
||||
# Pydantic options
|
||||
@ -366,13 +380,10 @@ class RevealJS(Converter):
|
||||
if isinstance(self.template, Path):
|
||||
return self.template.read_text()
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
return resources.read_text(templates, "revealjs.html")
|
||||
|
||||
return resources.files(templates).joinpath("revealjs.html").read_text()
|
||||
|
||||
def open(self, file: Path) -> bool:
|
||||
return webbrowser.open(file.absolute().as_uri())
|
||||
def open(self, file: Path) -> None:
|
||||
webbrowser.open(file.absolute().as_uri())
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""
|
||||
@ -395,15 +406,35 @@ class RevealJS(Converter):
|
||||
|
||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for presentation_config in self.presentation_configs:
|
||||
presentation_config.copy_to(full_assets_dir, include_reversed=False)
|
||||
num_presentation_configs = len(self.presentation_configs)
|
||||
|
||||
if num_presentation_configs > 1:
|
||||
# Prevent possible name collision, see:
|
||||
# https://github.com/jeertmans/manim-slides/issues/428
|
||||
# With ManimCE, this can happen when caching is disabled as filenames are
|
||||
# 'uncached_000x.mp4'
|
||||
# With ManimGL, this can easily occur since filenames are just basic integers...
|
||||
num_digits = len(str(num_presentation_configs - 1))
|
||||
|
||||
def prefix(i: int) -> str:
|
||||
return f"s{i:0{num_digits}d}_"
|
||||
|
||||
else:
|
||||
|
||||
def prefix(i: int) -> str:
|
||||
return ""
|
||||
|
||||
for i, presentation_config in enumerate(self.presentation_configs):
|
||||
presentation_config.copy_to(
|
||||
full_assets_dir, include_reversed=False, prefix=prefix(i)
|
||||
)
|
||||
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(dest, "w") as f:
|
||||
revealjs_template = Template(self.load_template())
|
||||
|
||||
options = self.dict()
|
||||
options = self.model_dump()
|
||||
options["assets_dir"] = assets_dir
|
||||
|
||||
has_notes = any(
|
||||
@ -423,6 +454,24 @@ class RevealJS(Converter):
|
||||
f.write(content)
|
||||
|
||||
|
||||
class HtmlZip(RevealJS):
|
||||
def open(self, file: Path) -> None:
|
||||
super(RevealJS, self).open(file) # Override opening with web browser
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""
|
||||
Convert this configuration into a zipped RevealJS HTML presentation, saved to
|
||||
DEST.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as directory_name:
|
||||
directory = Path(directory_name)
|
||||
|
||||
html_file = directory / dest.with_suffix(".html").name
|
||||
|
||||
super().convert_to(html_file)
|
||||
shutil.make_archive(str(dest.with_suffix("")), "zip", directory_name)
|
||||
|
||||
|
||||
class FrameIndex(str, Enum):
|
||||
first = "first"
|
||||
last = "last"
|
||||
@ -433,28 +482,8 @@ class PDF(Converter):
|
||||
resolution: PositiveFloat = 100.0
|
||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||
|
||||
def open(self, file: Path) -> None:
|
||||
return open_with_default(file)
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Convert this configuration into a PDF presentation, saved to DEST."""
|
||||
|
||||
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
|
||||
cap = cv2.VideoCapture(str(file))
|
||||
|
||||
if frame_index == FrameIndex.last:
|
||||
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
|
||||
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
if ret:
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
return Image.fromarray(frame)
|
||||
else:
|
||||
raise ValueError("Failed to read {image_index} image from video file")
|
||||
|
||||
images = []
|
||||
|
||||
for i, presentation_config in enumerate(self.presentation_configs):
|
||||
@ -487,10 +516,7 @@ class PowerPoint(Converter):
|
||||
poster_frame_image: Optional[FilePath] = None
|
||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||
|
||||
def open(self, file: Path) -> None:
|
||||
return open_with_default(file)
|
||||
|
||||
def convert_to(self, dest: Path) -> None: # noqa: C901
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
|
||||
prs = pptx.Presentation()
|
||||
prs.slide_width = self.width * 9525
|
||||
@ -506,7 +532,7 @@ class PowerPoint(Converter):
|
||||
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
|
||||
el_cnt = xpath(
|
||||
media.element.getparent().getparent().getparent(),
|
||||
'.//p:timing//p:video//p:spTgt[@spid="%s"]' % el_id,
|
||||
f'.//p:timing//p:video//p:spTgt[@spid="{el_id}"]',
|
||||
)[0]
|
||||
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
|
||||
cond.set("delay", "0")
|
||||
@ -519,53 +545,48 @@ class PowerPoint(Converter):
|
||||
nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
|
||||
return etree.ElementBase.xpath(el, query, namespaces=nsmap)
|
||||
|
||||
def save_first_image_from_video_file(file: Path) -> Optional[str]:
|
||||
cap = cv2.VideoCapture(file.as_posix())
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
with tempfile.TemporaryDirectory() as directory_name:
|
||||
directory = Path(directory_name)
|
||||
frame_number = 0
|
||||
for i, presentation_config in enumerate(self.presentation_configs):
|
||||
for slide_config in tqdm(
|
||||
presentation_config.slides,
|
||||
desc=f"Generating video slides for config {i + 1}",
|
||||
leave=False,
|
||||
):
|
||||
file = slide_config.file
|
||||
|
||||
if ret:
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
|
||||
cv2.imwrite(f.name, frame)
|
||||
f.close()
|
||||
return f.name
|
||||
else:
|
||||
logger.warn("Failed to read first image from video file")
|
||||
return None
|
||||
mime_type = mimetypes.guess_type(file)[0]
|
||||
|
||||
for i, presentation_config in enumerate(self.presentation_configs):
|
||||
for slide_config in tqdm(
|
||||
presentation_config.slides,
|
||||
desc=f"Generating video slides for config {i + 1}",
|
||||
leave=False,
|
||||
):
|
||||
file = slide_config.file
|
||||
if self.poster_frame_image is None:
|
||||
poster_frame_image = str(directory / f"{frame_number}.png")
|
||||
image = read_image_from_video_file(
|
||||
file, frame_index=FrameIndex.first
|
||||
)
|
||||
image.save(poster_frame_image)
|
||||
|
||||
mime_type = mimetypes.guess_type(file)[0]
|
||||
frame_number += 1
|
||||
else:
|
||||
poster_frame_image = str(self.poster_frame_image)
|
||||
|
||||
if self.poster_frame_image is None:
|
||||
poster_frame_image = save_first_image_from_video_file(file)
|
||||
else:
|
||||
poster_frame_image = str(self.poster_frame_image)
|
||||
slide = prs.slides.add_slide(layout)
|
||||
movie = slide.shapes.add_movie(
|
||||
str(file),
|
||||
self.left,
|
||||
self.top,
|
||||
self.width * 9525,
|
||||
self.height * 9525,
|
||||
poster_frame_image=poster_frame_image,
|
||||
mime_type=mime_type,
|
||||
)
|
||||
if slide_config.notes != "":
|
||||
slide.notes_slide.notes_text_frame.text = slide_config.notes
|
||||
|
||||
slide = prs.slides.add_slide(layout)
|
||||
movie = slide.shapes.add_movie(
|
||||
str(file),
|
||||
self.left,
|
||||
self.top,
|
||||
self.width * 9525,
|
||||
self.height * 9525,
|
||||
poster_frame_image=poster_frame_image,
|
||||
mime_type=mime_type,
|
||||
)
|
||||
if slide_config.notes != "":
|
||||
slide.notes_slide.notes_text_frame.text = slide_config.notes
|
||||
if self.auto_play_media:
|
||||
auto_play_media(movie, loop=slide_config.loop)
|
||||
|
||||
if self.auto_play_media:
|
||||
auto_play_media(movie, loop=slide_config.loop)
|
||||
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
prs.save(dest)
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
prs.save(dest)
|
||||
|
||||
|
||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@ -577,11 +598,14 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
|
||||
to = ctx.params.get("to", "html")
|
||||
|
||||
converter = Converter.from_string(to)(
|
||||
presentation_configs=[PresentationConfig()]
|
||||
)
|
||||
for key, value in converter.dict().items():
|
||||
click.echo(f"{key}: {value!r}")
|
||||
converter = Converter.from_string(to)
|
||||
|
||||
for key, field in converter.model_fields.items():
|
||||
if field.is_required():
|
||||
continue
|
||||
|
||||
default = field.get_default(call_default_factory=True)
|
||||
click.echo(f"{key}: {default}")
|
||||
|
||||
ctx.exit()
|
||||
|
||||
@ -630,7 +654,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--to",
|
||||
type=click.Choice(["auto", "html", "pdf", "pptx"], case_sensitive=False),
|
||||
type=click.Choice(["auto", "html", "pdf", "pptx", "zip"], case_sensitive=False),
|
||||
metavar="FORMAT",
|
||||
default="auto",
|
||||
show_default=True,
|
||||
@ -640,7 +664,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"--open",
|
||||
"open_result",
|
||||
is_flag=True,
|
||||
help="Open the newly created file using the approriate 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(
|
||||
@ -664,13 +688,13 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@show_config_options
|
||||
@verbosity_option
|
||||
def convert(
|
||||
scenes: List[str],
|
||||
scenes: list[str],
|
||||
folder: Path,
|
||||
dest: Path,
|
||||
to: str,
|
||||
open_result: bool,
|
||||
force: bool,
|
||||
config_options: Dict[str, str],
|
||||
config_options: dict[str, str],
|
||||
template: Optional[Path],
|
||||
) -> None:
|
||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||
@ -682,7 +706,7 @@ def convert(
|
||||
try:
|
||||
cls = Converter.from_string(fmt)
|
||||
except KeyError:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||
)
|
||||
cls = RevealJS
|
||||
@ -704,7 +728,7 @@ def convert(
|
||||
errors = e.errors()
|
||||
|
||||
msg = [
|
||||
f"{len(errors)} error(s) occured with configuration options for '{to}', see below."
|
||||
f"{len(errors)} error(s) occurred with configuration options for '{to}', see below."
|
||||
]
|
||||
|
||||
for error in errors:
|
||||
|
@ -86,6 +86,19 @@ A third application is to render scenes from another specific file::
|
||||
you should probably not include examples that rely on external files, since
|
||||
relative paths risk to be broken.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to skip rendering the slides (e.g., for testing)
|
||||
you can either set the ``SKIP_MANIM_SLIDES`` environ
|
||||
variable (to any value) or pass the ``skip-manim-slides``
|
||||
tag to ``sphinx``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sphinx-build -t skip-manim-slides <OTHER_SPHINX_OPTIONS>
|
||||
# or if you use a Makefile
|
||||
make html O=-tskip-manim-slides
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
@ -176,10 +189,12 @@ Renders as follows:
|
||||
|
||||
|
||||
""" # noqa: D400, D415
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import itertools as it
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
@ -268,9 +283,9 @@ class ManimSlidesDirective(Directive):
|
||||
# Rendering is skipped if the tag skip-manim is present,
|
||||
# or if we are making the pot-files
|
||||
should_skip = (
|
||||
"skip-manim-slides"
|
||||
in self.state.document.settings.env.app.builder.tags.tags
|
||||
self.state.document.settings.env.app.builder.tags.has("skip-manim-slides")
|
||||
or self.state.document.settings.env.app.builder.name == "gettext"
|
||||
or "SKIP_MANIM_SLIDES" in os.environ
|
||||
)
|
||||
if should_skip:
|
||||
node = SkipManimNode()
|
||||
@ -440,7 +455,7 @@ def _write_rendering_stats(scene_name, run_time, file_name):
|
||||
[
|
||||
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
|
||||
scene_name,
|
||||
"%.3f" % run_time,
|
||||
f"{run_time:.3f}",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -228,7 +228,7 @@ class ManimSlidesMagic(Magics): # type: ignore
|
||||
# TODO: FIXME
|
||||
# Seems like files are blocked so date-uri is the only working option...
|
||||
if kwargs.get("data_uri", "false").lower().strip() == "false":
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
"data_uri option is currently automatically enabled, "
|
||||
"because using local video files does not seem to work properly."
|
||||
)
|
||||
@ -249,9 +249,7 @@ class ManimSlidesMagic(Magics): # type: ignore
|
||||
)
|
||||
else:
|
||||
result = HTML(
|
||||
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{src}"></iframe></div>""".format(
|
||||
src=tmpfile.as_posix()
|
||||
)
|
||||
f"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{tmpfile.as_posix()}"></iframe></div>"""
|
||||
)
|
||||
|
||||
display(result)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
from click import Context, Parameter
|
||||
@ -11,23 +11,6 @@ from ..commons import config_path_option, folder_path_option, verbosity_option
|
||||
from ..config import Config, PresentationConfig
|
||||
from ..logger import logger
|
||||
|
||||
PREFERRED_QT_VERSIONS = ("6.5.1", "6.5.2")
|
||||
|
||||
|
||||
def warn_if_non_desirable_pyside6_version() -> None:
|
||||
from qtpy import API, QT_VERSION
|
||||
|
||||
if sys.version_info < (3, 12) and (
|
||||
API != "pyside6" or QT_VERSION not in PREFERRED_QT_VERSIONS
|
||||
):
|
||||
logger.warn(
|
||||
f"You are using {API = }, {QT_VERSION = }, "
|
||||
"but we recommend installing 'PySide6==6.5.2', mainly to avoid "
|
||||
"flashing screens between slides, "
|
||||
"see issue https://github.com/jeertmans/manim-slides/issues/293. "
|
||||
"You can do so with `pip install 'manim-slides[pyside6]'`."
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@folder_path_option
|
||||
@ -39,7 +22,7 @@ def list_scenes(folder: Path) -> None:
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
|
||||
def _list_scenes(folder: Path) -> List[str]:
|
||||
def _list_scenes(folder: Path) -> list[str]:
|
||||
"""List available scenes in given directory."""
|
||||
scenes = []
|
||||
|
||||
@ -50,7 +33,7 @@ def _list_scenes(folder: Path) -> List[str]:
|
||||
except (
|
||||
Exception
|
||||
) as e: # Could not parse this file as a proper presentation config
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
|
||||
)
|
||||
|
||||
@ -59,7 +42,7 @@ def _list_scenes(folder: Path) -> List[str]:
|
||||
return scenes
|
||||
|
||||
|
||||
def prompt_for_scenes(folder: Path) -> List[str]:
|
||||
def prompt_for_scenes(folder: Path) -> list[str]:
|
||||
"""Prompt the user to select scenes within a given folder."""
|
||||
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
|
||||
|
||||
@ -71,7 +54,7 @@ def prompt_for_scenes(folder: Path) -> List[str]:
|
||||
click.echo("Choose number corresponding to desired scene/arguments.")
|
||||
click.echo("(Use comma separated list for multiple entries)")
|
||||
|
||||
def value_proc(value: Optional[str]) -> List[str]:
|
||||
def value_proc(value: Optional[str]) -> list[str]:
|
||||
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
|
||||
|
||||
if not all(0 < i <= len(scene_choices) for i in indices):
|
||||
@ -93,8 +76,8 @@ def prompt_for_scenes(folder: Path) -> List[str]:
|
||||
|
||||
|
||||
def get_scenes_presentation_config(
|
||||
scenes: List[str], folder: Path
|
||||
) -> List[PresentationConfig]:
|
||||
scenes: list[str], folder: Path
|
||||
) -> list[PresentationConfig]:
|
||||
"""Return a list of presentation configurations based on the user input."""
|
||||
if len(scenes) == 0:
|
||||
scenes = prompt_for_scenes(folder)
|
||||
@ -116,7 +99,7 @@ def get_scenes_presentation_config(
|
||||
|
||||
def start_at_callback(
|
||||
ctx: Context, param: Parameter, values: str
|
||||
) -> Tuple[Optional[int], ...]:
|
||||
) -> tuple[Optional[int], ...]:
|
||||
if values == "(None, None)":
|
||||
return (None, None)
|
||||
|
||||
@ -242,10 +225,18 @@ def start_at_callback(
|
||||
is_flag=True,
|
||||
help="Hide info window.",
|
||||
)
|
||||
@click.option(
|
||||
"--info-window-screen",
|
||||
"info_window_screen_number",
|
||||
metavar="NUMBER",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Put info window on the given screen (a.k.a. display).",
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def present(
|
||||
scenes: List[str],
|
||||
scenes: list[str],
|
||||
config_path: Path,
|
||||
folder: Path,
|
||||
start_paused: bool,
|
||||
@ -254,13 +245,14 @@ def present(
|
||||
exit_after_last_slide: bool,
|
||||
hide_mouse: bool,
|
||||
aspect_ratio: str,
|
||||
start_at: Tuple[Optional[int], Optional[int], Optional[int]],
|
||||
start_at: tuple[Optional[int], Optional[int], Optional[int]],
|
||||
start_at_scene_number: int,
|
||||
start_at_slide_number: int,
|
||||
screen_number: Optional[int],
|
||||
playback_rate: float,
|
||||
next_terminates_loop: bool,
|
||||
hide_info_window: bool,
|
||||
info_window_screen_number: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
Present SCENE(s), one at a time, in order.
|
||||
@ -293,34 +285,40 @@ def present(
|
||||
if start_at[1]:
|
||||
start_at_slide_number = start_at[1]
|
||||
|
||||
warn_if_non_desirable_pyside6_version()
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QScreen
|
||||
|
||||
from ..qt_utils import qapp
|
||||
from .player import Player
|
||||
|
||||
app = qapp()
|
||||
app.setApplicationName("Manim Slides")
|
||||
|
||||
if screen_number is not None:
|
||||
def get_screen(number: int) -> Optional[QScreen]:
|
||||
try:
|
||||
screen = app.screens()[screen_number]
|
||||
return app.screens()[number]
|
||||
except IndexError:
|
||||
logger.error(
|
||||
f"Invalid screen number {screen_number}, "
|
||||
f"Invalid screen number {number}, "
|
||||
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
|
||||
)
|
||||
screen = None
|
||||
return None
|
||||
|
||||
if screen_number is not None:
|
||||
screen = get_screen(screen_number)
|
||||
else:
|
||||
screen = None
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
if info_window_screen_number is not None:
|
||||
info_window_screen = get_screen(info_window_screen_number)
|
||||
else:
|
||||
info_window_screen = None
|
||||
|
||||
aspect_ratio_modes = {
|
||||
"keep": Qt.KeepAspectRatio,
|
||||
"ignore": Qt.IgnoreAspectRatio,
|
||||
}
|
||||
|
||||
from .player import Player
|
||||
|
||||
player = Player(
|
||||
config,
|
||||
presentation_configs,
|
||||
@ -336,6 +334,7 @@ def present(
|
||||
playback_rate=playback_rate,
|
||||
next_terminates_loop=next_terminates_loop,
|
||||
hide_info_window=hide_info_window,
|
||||
info_window_screen=info_window_screen,
|
||||
)
|
||||
|
||||
player.show()
|
||||
|
@ -1,10 +1,10 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
|
||||
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
|
||||
from qtpy.QtMultimedia import QMediaPlayer
|
||||
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer, QVideoFrame
|
||||
from qtpy.QtMultimediaWidgets import QVideoWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
@ -56,7 +56,7 @@ class Info(QWidget): # type: ignore[misc]
|
||||
self.video_sink = main_video_widget.videoSink()
|
||||
left_layout.addWidget(main_video_widget)
|
||||
|
||||
# Current slide informations
|
||||
# Current slide information
|
||||
|
||||
self.scene_label = QLabel()
|
||||
self.slide_label = QLabel()
|
||||
@ -169,7 +169,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
presentation_configs: List[PresentationConfig],
|
||||
presentation_configs: list[PresentationConfig],
|
||||
*,
|
||||
start_paused: bool = False,
|
||||
full_screen: bool = False,
|
||||
@ -183,6 +183,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
playback_rate: float = 1.0,
|
||||
next_terminates_loop: bool = False,
|
||||
hide_info_window: bool = False,
|
||||
info_window_screen: Optional[QScreen] = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
@ -225,12 +226,16 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
self.frame = QVideoFrame()
|
||||
|
||||
self.audio_output = QAudioOutput()
|
||||
self.video_widget = QVideoWidget()
|
||||
self.video_sink = self.video_widget.videoSink()
|
||||
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
|
||||
self.setCentralWidget(self.video_widget)
|
||||
|
||||
self.media_player = QMediaPlayer(self)
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
self.media_player.setVideoOutput(self.video_widget)
|
||||
self.playback_rate = playback_rate
|
||||
|
||||
@ -238,13 +243,13 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
self.slide_changed.connect(self.slide_changed_callback)
|
||||
|
||||
self.info = Info(
|
||||
full_screen=full_screen, aspect_ratio_mode=aspect_ratio_mode, screen=screen
|
||||
full_screen=full_screen,
|
||||
aspect_ratio_mode=aspect_ratio_mode,
|
||||
screen=info_window_screen,
|
||||
)
|
||||
self.info.close_event.connect(self.closeEvent)
|
||||
self.info.key_press_event.connect(self.keyPressEvent)
|
||||
self.video_sink.videoFrameChanged.connect(
|
||||
lambda frame: self.info.video_sink.setVideoFrame(frame)
|
||||
)
|
||||
self.video_sink.videoFrameChanged.connect(self.frame_changed)
|
||||
self.hide_info_window = hide_info_window
|
||||
|
||||
# Connecting key callbacks
|
||||
@ -314,7 +319,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
elif -self.presentations_count <= index < 0:
|
||||
self.__current_presentation_index = index + self.presentations_count
|
||||
else:
|
||||
logger.warn(f"Could not set presentation index to {index}.")
|
||||
logger.warning(f"Could not set presentation index to {index}.")
|
||||
return
|
||||
|
||||
self.presentation_changed.emit()
|
||||
@ -338,7 +343,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
elif -self.current_slides_count <= index < 0:
|
||||
self.__current_slide_index = index + self.current_slides_count
|
||||
else:
|
||||
logger.warn(f"Could not set slide index to {index}.")
|
||||
logger.warning(f"Could not set slide index to {index}.")
|
||||
return
|
||||
|
||||
self.slide_changed.emit()
|
||||
@ -540,6 +545,34 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
else:
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
def frame_changed(self, frame: QVideoFrame) -> None:
|
||||
"""
|
||||
Slot to handle possibly invalid frames.
|
||||
|
||||
This slot cannot be decorated with ``@Slot`` as
|
||||
the video sinks are handled in different threads.
|
||||
|
||||
As of Qt>=6.5.3, the last frame of every video is "flushed",
|
||||
resulting in a short black screen between each slide.
|
||||
|
||||
To avoid this issue, we check every frame, and avoid playing
|
||||
invalid ones.
|
||||
|
||||
References
|
||||
----------
|
||||
1. https://github.com/jeertmans/manim-slides/issues/293
|
||||
2. https://github.com/jeertmans/manim-slides/pull/464
|
||||
|
||||
:param frame: The most recent frame.
|
||||
|
||||
"""
|
||||
if frame.isValid():
|
||||
self.frame = frame
|
||||
else:
|
||||
self.video_sink.setVideoFrame(self.frame) # Reuse previous frame
|
||||
|
||||
self.info.video_sink.setVideoFrame(self.frame)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||
self.close()
|
||||
|
||||
|
@ -12,7 +12,6 @@ This is especially useful for two reasons:
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
import click
|
||||
|
||||
@ -39,7 +38,7 @@ import click
|
||||
help="If set, use ManimGL renderer.",
|
||||
)
|
||||
@click.argument("args", metavar="[RENDERER_ARGS]...", nargs=-1, type=click.UNPROCESSED)
|
||||
def render(ce: bool, gl: bool, args: Tuple[str, ...]) -> None:
|
||||
def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
|
||||
"""
|
||||
Render SCENE(s) from the input FILE, using the specified renderer.
|
||||
|
||||
|
@ -11,7 +11,8 @@ that directly calls ``self.play(Animation(...))``, see
|
||||
|
||||
__all__ = ["Wipe", "Zoom"]
|
||||
|
||||
from typing import Any, Mapping, Optional, Sequence
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
@ -3,14 +3,13 @@ from __future__ import annotations
|
||||
__all__ = ["BaseSlide"]
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
from abc import abstractmethod
|
||||
from collections.abc import MutableMapping, Sequence, ValuesView
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
MutableMapping,
|
||||
Sequence,
|
||||
ValuesView,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
@ -34,6 +33,10 @@ LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
|
||||
|
||||
|
||||
class BaseSlide:
|
||||
disable_caching: bool = False
|
||||
flush_cache: bool = False
|
||||
skip_reversing: bool = False
|
||||
|
||||
def __init__(
|
||||
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
|
||||
) -> None:
|
||||
@ -172,11 +175,23 @@ class BaseSlide:
|
||||
animations. You must still call :code:`self.add` or
|
||||
play some animation that introduces each Mobject for
|
||||
it to appear. The same applies when removing objects.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`canvas` for usage examples.
|
||||
"""
|
||||
self._canvas.update(objects)
|
||||
|
||||
def remove_from_canvas(self, *names: str) -> None:
|
||||
"""Remove objects from the canvas."""
|
||||
"""
|
||||
Remove objects from the canvas.
|
||||
|
||||
:param names: The names of objects to remove.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`canvas` for usage examples.
|
||||
"""
|
||||
for name in names:
|
||||
self._canvas.pop(name)
|
||||
|
||||
@ -188,8 +203,12 @@ class BaseSlide:
|
||||
@property
|
||||
def mobjects_without_canvas(self) -> Sequence[Mobject]:
|
||||
"""
|
||||
Return the list of objects contained in the scene, minus those present in
|
||||
Return the list of Mobjects contained in the scene, minus those present in
|
||||
the canvas.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`canvas` for usage examples.
|
||||
"""
|
||||
return [
|
||||
mobject
|
||||
@ -277,7 +296,7 @@ class BaseSlide:
|
||||
next slide is played. By default, this is the right arrow key.
|
||||
|
||||
:param args:
|
||||
Positional arguments to be passed to
|
||||
Positional arguments passed to
|
||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||
or ignored if `manimlib` API is used.
|
||||
:param loop:
|
||||
@ -286,27 +305,38 @@ class BaseSlide:
|
||||
If set, next slide will play immediately play the next slide
|
||||
upon terminating.
|
||||
|
||||
Note that this is only supported by ``manim-slides present``
|
||||
and ``manim-slides convert --to=html``.
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``
|
||||
and ``manim-slides convert --to=html``.
|
||||
:param playback_rate:
|
||||
Playback rate at which the video is played.
|
||||
|
||||
Note that this is only supported by ``manim-slides present``.
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``.
|
||||
:param reversed_playback_rate:
|
||||
Playback rate at which the reversed video is played.
|
||||
|
||||
Note that this is only supported by ``manim-slides present``.
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``.
|
||||
:param notes:
|
||||
Presenter notes, in Markdown format.
|
||||
|
||||
Note that PowerPoint does not support Markdown.
|
||||
.. note::
|
||||
PowerPoint does not support Markdown formatting,
|
||||
so the text will be displayed as is.
|
||||
|
||||
Note that this is only supported by ``manim-slides present``
|
||||
and ``manim-slides convert --to=html/pptx``.
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``,
|
||||
``manim-slides convert --to=html`` and
|
||||
``manim-slides convert --to=pptx``.
|
||||
:param dedent_notes:
|
||||
If set, apply :func:`textwrap.dedent` to notes.
|
||||
:param kwargs:
|
||||
Keyword arguments to be passed to
|
||||
Keyword arguments passed to
|
||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||
or ignored if `manimlib` API is used.
|
||||
|
||||
@ -447,11 +477,17 @@ class BaseSlide:
|
||||
)
|
||||
)
|
||||
|
||||
def _save_slides(self, use_cache: bool = True) -> None:
|
||||
def _save_slides(
|
||||
self,
|
||||
use_cache: bool = True,
|
||||
flush_cache: bool = False,
|
||||
skip_reversing: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Save slides, optionally using cached files.
|
||||
|
||||
Note that cached files only work with Manim.
|
||||
.. warning:
|
||||
Caching files only work with Manim.
|
||||
"""
|
||||
self._add_last_slide()
|
||||
|
||||
@ -460,6 +496,9 @@ class BaseSlide:
|
||||
scene_name = str(self)
|
||||
scene_files_folder = files_folder / scene_name
|
||||
|
||||
if flush_cache and scene_files_folder.exists():
|
||||
shutil.rmtree(scene_files_folder)
|
||||
|
||||
scene_files_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files: list[Path] = self._partial_movie_files
|
||||
@ -494,7 +533,10 @@ class BaseSlide:
|
||||
|
||||
# We only reverse video if it was not present
|
||||
if not use_cache or not rev_file.exists():
|
||||
reverse_video_file(dst_file, rev_file)
|
||||
if skip_reversing:
|
||||
rev_file = dst_file
|
||||
else:
|
||||
reverse_video_file(dst_file, rev_file)
|
||||
|
||||
slides.append(
|
||||
SlideConfig.from_pre_slide_config_and_files(
|
||||
|
@ -1,7 +1,9 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
from manim import Scene, ThreeDScene, config
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.utils.color import rgba_to_color
|
||||
|
||||
from ..config import BaseSlideConfig
|
||||
from .base import BaseSlide
|
||||
@ -11,30 +13,61 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
"""
|
||||
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
|
||||
for slides rendering.
|
||||
|
||||
:param args: Positional arguments passed to scene object.
|
||||
:param output_folder: Where the slide animation files should be written.
|
||||
:param kwargs: Keyword arguments passed to scene object.
|
||||
:cvar bool disable_caching: :data:`False`: Whether to disable the use of
|
||||
cached animation files.
|
||||
:cvar bool flush_cache: :data:`False`: Whether to flush the cache.
|
||||
|
||||
Unlike with Manim, flushing is performed before rendering.
|
||||
:cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations.
|
||||
|
||||
If set to :data:`False`, and no cached reversed animation
|
||||
exists (or caching is disabled) for a given slide,
|
||||
then the reversed animation will be simply the same
|
||||
as the original one, i.e., ``rev_file = file``,
|
||||
for the current slide config.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _frame_shape(self) -> tuple[float, float]:
|
||||
if isinstance(self.renderer, OpenGLRenderer):
|
||||
return self.renderer.camera.frame_shape # type: ignore
|
||||
else:
|
||||
return (
|
||||
self.renderer.camera.frame_height,
|
||||
self.renderer.camera.frame_width,
|
||||
)
|
||||
|
||||
@property
|
||||
def _frame_height(self) -> float:
|
||||
return config["frame_height"] # type: ignore
|
||||
return self._frame_shape[0]
|
||||
|
||||
@property
|
||||
def _frame_width(self) -> float:
|
||||
return config["frame_width"] # type: ignore
|
||||
return self._frame_shape[1]
|
||||
|
||||
@property
|
||||
def _background_color(self) -> str:
|
||||
color = config["background_color"]
|
||||
if hex_color := getattr(color, "hex", None):
|
||||
return hex_color # type: ignore
|
||||
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
|
||||
return color.to_hex() # type: ignore
|
||||
if isinstance(self.renderer, OpenGLRenderer):
|
||||
return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore
|
||||
else:
|
||||
return self.renderer.camera.background_color.to_hex() # type: ignore
|
||||
|
||||
@property
|
||||
def _resolution(self) -> Tuple[int, int]:
|
||||
return config["pixel_width"], config["pixel_height"]
|
||||
def _resolution(self) -> tuple[int, int]:
|
||||
if isinstance(self.renderer, OpenGLRenderer):
|
||||
return self.renderer.get_pixel_shape() # type: ignore
|
||||
else:
|
||||
return (
|
||||
self.renderer.camera.pixel_width,
|
||||
self.renderer.camera.pixel_height,
|
||||
)
|
||||
|
||||
@property
|
||||
def _partial_movie_files(self) -> List[Path]:
|
||||
def _partial_movie_files(self) -> list[Path]:
|
||||
# When rendering with -na,b (manim only)
|
||||
# the animations not in [a,b] will be skipped,
|
||||
# but animation before a will have a None source file.
|
||||
@ -85,16 +118,29 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
)
|
||||
|
||||
def render(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""MANIM render."""
|
||||
"""MANIM renderer."""
|
||||
# We need to disable the caching limit since we rely on intermediate files
|
||||
max_files_cached = config["max_files_cached"]
|
||||
config["max_files_cached"] = float("inf")
|
||||
|
||||
flush_manim_cache = config["flush_cache"]
|
||||
|
||||
if flush_manim_cache:
|
||||
# We need to postpone flushing *after* we saved slides
|
||||
config["flush_cache"] = False
|
||||
|
||||
super().render(*args, **kwargs)
|
||||
|
||||
config["max_files_cached"] = max_files_cached
|
||||
|
||||
self._save_slides()
|
||||
self._save_slides(
|
||||
use_cache=not (config["disable_caching"] or self.disable_caching),
|
||||
flush_cache=(config["flush_cache"] or self.flush_cache),
|
||||
skip_reversing=self.skip_reversing,
|
||||
)
|
||||
|
||||
if flush_manim_cache:
|
||||
self.renderer.file_writer.flush_cache_directory()
|
||||
|
||||
|
||||
class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
|
||||
|
@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Tuple
|
||||
from typing import Any, ClassVar, Optional
|
||||
|
||||
from manimlib import Scene, ThreeDCamera
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
@ -31,11 +31,11 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
return self.camera_config["background_color"].hex # type: ignore
|
||||
|
||||
@property
|
||||
def _resolution(self) -> Tuple[int, int]:
|
||||
def _resolution(self) -> tuple[int, int]:
|
||||
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
|
||||
|
||||
@property
|
||||
def _partial_movie_files(self) -> List[Path]:
|
||||
def _partial_movie_files(self) -> list[Path]:
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.file_writer.movie_file_extension,
|
||||
@ -62,11 +62,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""MANIMGL renderer."""
|
||||
super().run(*args, **kwargs)
|
||||
self._save_slides(use_cache=False)
|
||||
self._save_slides(
|
||||
use_cache=False,
|
||||
flush_cache=self.flush_cache,
|
||||
skip_reversing=self.skip_reversing,
|
||||
)
|
||||
|
||||
|
||||
class ThreeDSlide(Slide):
|
||||
CONFIG: ClassVar[Dict[str, Any]] = {
|
||||
CONFIG: ClassVar[dict[str, Any]] = {
|
||||
"camera_class": ThreeDCamera,
|
||||
}
|
||||
pass
|
||||
|
@ -33,7 +33,7 @@
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||
data-background-video-muted
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
{% if slide_config.loop -%}
|
||||
data-background-video-loop
|
||||
{%- endif -%}
|
||||
@ -262,7 +262,7 @@
|
||||
mouseWheel: {{ mouse_wheel }},
|
||||
|
||||
// Opens links in an iframe preview overlay
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
||||
// individually
|
||||
previewLinks: {{ preview_links }},
|
||||
|
||||
|
@ -1,40 +1,68 @@
|
||||
import hashlib
|
||||
import os
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import av
|
||||
|
||||
from .logger import logger
|
||||
|
||||
|
||||
def concatenate_video_files(files: List[Path], dest: Path) -> None:
|
||||
def concatenate_video_files(files: list[Path], dest: Path) -> None:
|
||||
"""Concatenate multiple video files into one."""
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
||||
f.writelines(f"file '{path.absolute()}'\n" for path in files)
|
||||
f.close()
|
||||
|
||||
input_ = av.open(f.name, options={"safe": "0"}, format="concat")
|
||||
input_stream = input_.streams.video[0]
|
||||
output = av.open(str(dest), mode="w")
|
||||
output_stream = output.add_stream(
|
||||
template=input_stream,
|
||||
)
|
||||
def _filter(files: list[Path]) -> Iterator[Path]:
|
||||
"""Patch possibly empty video files."""
|
||||
for file in files:
|
||||
with av.open(str(file)) as container:
|
||||
if len(container.streams.video) > 0:
|
||||
yield file
|
||||
else:
|
||||
logger.warning(
|
||||
f"Skipping video file {file} because it does "
|
||||
"not contain any video stream. "
|
||||
"This is probably caused by Manim, see: "
|
||||
"https://github.com/jeertmans/manim-slides/issues/390."
|
||||
)
|
||||
|
||||
for packet in input_.demux(input_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
||||
f.writelines(f"file '{file}'\n" for file in _filter(files))
|
||||
tmp_file = f.name
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_stream
|
||||
output.mux(packet)
|
||||
with (
|
||||
av.open(tmp_file, format="concat", options={"safe": "0"}) as input_container,
|
||||
av.open(str(dest), mode="w") as output_container,
|
||||
):
|
||||
input_video_stream = input_container.streams.video[0]
|
||||
output_video_stream = output_container.add_stream(
|
||||
template=input_video_stream,
|
||||
)
|
||||
|
||||
input_.close()
|
||||
output.close()
|
||||
if len(input_container.streams.audio) > 0:
|
||||
input_audio_stream = input_container.streams.audio[0]
|
||||
output_audio_stream = output_container.add_stream(
|
||||
template=input_audio_stream,
|
||||
)
|
||||
|
||||
for packet in input_container.demux():
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
ptype = packet.stream.type
|
||||
|
||||
if ptype == "video":
|
||||
packet.stream = output_video_stream
|
||||
elif ptype == "audio":
|
||||
packet.stream = output_audio_stream
|
||||
else:
|
||||
continue # We don't support subtitles
|
||||
output_container.mux(packet)
|
||||
|
||||
os.unlink(tmp_file) # https://stackoverflow.com/a/54768241
|
||||
|
||||
|
||||
def merge_basenames(files: List[Path]) -> Path:
|
||||
def merge_basenames(files: list[Path]) -> Path:
|
||||
"""Merge multiple filenames by concatenating basenames."""
|
||||
if len(files) == 0:
|
||||
raise ValueError("Cannot merge an empty list of files!")
|
||||
@ -62,37 +90,38 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
|
||||
|
||||
|
||||
def reverse_video_file(src: Path, dest: Path) -> None:
|
||||
"""Reverses a video file, writting the result to `dest`."""
|
||||
input_ = av.open(str(src))
|
||||
input_stream = input_.streams.video[0]
|
||||
output = av.open(str(dest), mode="w")
|
||||
output_stream = output.add_stream(codec_name="libx264", rate=input_stream.base_rate)
|
||||
output_stream.width = input_stream.width
|
||||
output_stream.height = input_stream.height
|
||||
output_stream.pix_fmt = input_stream.pix_fmt
|
||||
"""Reverses a video file, writing the result to `dest`."""
|
||||
with (
|
||||
av.open(str(src)) as input_container,
|
||||
av.open(str(dest), mode="w") as output_container,
|
||||
):
|
||||
input_stream = input_container.streams.video[0]
|
||||
output_stream = output_container.add_stream(
|
||||
codec_name="libx264", rate=input_stream.base_rate
|
||||
)
|
||||
output_stream.width = input_stream.width
|
||||
output_stream.height = input_stream.height
|
||||
output_stream.pix_fmt = input_stream.pix_fmt
|
||||
|
||||
graph = av.filter.Graph()
|
||||
link_nodes(
|
||||
graph.add_buffer(template=input_stream),
|
||||
graph.add("reverse"),
|
||||
graph.add("buffersink"),
|
||||
)
|
||||
graph.configure()
|
||||
graph = av.filter.Graph()
|
||||
link_nodes(
|
||||
graph.add_buffer(template=input_stream),
|
||||
graph.add("reverse"),
|
||||
graph.add("buffersink"),
|
||||
)
|
||||
graph.configure()
|
||||
|
||||
frames_count = 0
|
||||
for frame in input_.decode(video=0):
|
||||
graph.push(frame)
|
||||
frames_count += 1
|
||||
frames_count = 0
|
||||
for frame in input_container.decode(video=0):
|
||||
graph.push(frame)
|
||||
frames_count += 1
|
||||
|
||||
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
||||
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
||||
|
||||
for _ in range(frames_count):
|
||||
frame = graph.pull()
|
||||
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
|
||||
output.mux(output_stream.encode(frame))
|
||||
for _ in range(frames_count):
|
||||
frame = graph.pull()
|
||||
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
|
||||
output_container.mux(output_stream.encode(frame))
|
||||
|
||||
for packet in output_stream.encode():
|
||||
output.mux(packet)
|
||||
|
||||
input_.close()
|
||||
output.close()
|
||||
for packet in output_stream.encode():
|
||||
output_container.mux(packet)
|
||||
|
@ -63,7 +63,7 @@ class Wizard(QWidget): # type: ignore
|
||||
|
||||
self.layout = QGridLayout()
|
||||
|
||||
for i, (key, value) in enumerate(self.config.keys.dict().items()):
|
||||
for i, (key, value) in enumerate(self.config.keys.model_dump().items()):
|
||||
# Create label for key name information
|
||||
label = QLabel()
|
||||
key_info = value["name"] or key
|
||||
@ -97,7 +97,7 @@ class Wizard(QWidget): # type: ignore
|
||||
|
||||
def save_config(self) -> None:
|
||||
try:
|
||||
Config.model_validate(self.config.dict())
|
||||
Config.model_validate(self.config.model_dump())
|
||||
except ValueError:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
|
@ -66,7 +66,7 @@ provide new features on a regular basis.
|
||||
|
||||
# Easy to Use Commitment
|
||||
|
||||
Manim Slides is commited to be an easy-to-use tool, with minimal installation
|
||||
Manim Slides is committed to be an easy-to-use tool, with minimal installation
|
||||
procedure and few modifications required. It can either be used locally with its
|
||||
graphical user interface (GUI), or shared via one of the two formats it can
|
||||
convert to:
|
||||
@ -96,13 +96,13 @@ and posted it on YouTube.
|
||||
|
||||
# Stability and releases
|
||||
|
||||
Manim Slides is continously tested on most recent Python versions, both ManimCE
|
||||
Manim Slides is continuously tested on most recent Python versions, both ManimCE
|
||||
and ManimGL, and on all major platforms: **Ubuntu**, **macOS** and **Windows**. Due to Manim
|
||||
Slide's exposed API being very minimal, and the variety of tests that are
|
||||
performed, this tool can be considered stable over time.
|
||||
|
||||
New releases are very frequent, as they mostly introduce enhancements or small
|
||||
documention fixes, and the command-line tool automatically notifies for new
|
||||
documentation fixes, and the command-line tool automatically notifies for new
|
||||
available updates. Therefore, regularly updating Manim Slides is highly
|
||||
recommended.
|
||||
|
||||
@ -160,7 +160,7 @@ For new feature requests, we highly encourage users to
|
||||
[create an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
|
||||
with the appropriate template.
|
||||
|
||||
# Acknowledgements
|
||||
# Acknowledgments
|
||||
|
||||
We acknowledge the work of @manim-presentation that paved the initial structure
|
||||
of Manim Slides with the manim-presentation Python package.
|
||||
|
167
pyproject.toml
167
pyproject.toml
@ -1,6 +1,6 @@
|
||||
[build-system]
|
||||
build-backend = "pdm.backend"
|
||||
requires = ["pdm-backend", "setuptools"]
|
||||
build-backend = "hatchling.build"
|
||||
requires = ["hatchling", "hatch-fancy-pypi-readme"]
|
||||
|
||||
[project]
|
||||
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
|
||||
@ -23,7 +23,6 @@ dependencies = [
|
||||
"jinja2>=3.1.2",
|
||||
"lxml>=4.9.2",
|
||||
"numpy>=1.19",
|
||||
"opencv-python>=4.6.0.66",
|
||||
"pillow>=9.5.0",
|
||||
"pydantic>=2.0.1",
|
||||
"pydantic-extra-types>=2.0.0",
|
||||
@ -31,20 +30,19 @@ dependencies = [
|
||||
"qtpy>=2.4.1",
|
||||
"requests>=2.28.1",
|
||||
"rich>=13.3.2",
|
||||
"rtoml>=0.9.0",
|
||||
"rtoml>=0.11.0",
|
||||
"tqdm>=4.64.1",
|
||||
]
|
||||
description = "Tool for live presentations using manim"
|
||||
dynamic = ["version"]
|
||||
dynamic = ["readme", "version"]
|
||||
keywords = ["manim", "slides", "plugin", "manimgl"]
|
||||
license = {text = "MIT"}
|
||||
license = "MIT"
|
||||
name = "manim-slides"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9,<3.13"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"manim-slides[magic,sphinx-directive]",
|
||||
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
|
||||
"furo>=2023.5.20",
|
||||
"ipykernel>=6.25.1",
|
||||
"myst-parser>=2.0.0",
|
||||
@ -56,16 +54,24 @@ docs = [
|
||||
"sphinxext-opengraph>=0.7.5",
|
||||
]
|
||||
full = [
|
||||
"manim-slides[magic,manim,manimgl,sphinx-directive]",
|
||||
"manim-slides[magic,manim,sphinx-directive]",
|
||||
]
|
||||
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
|
||||
manim = ["manim>=0.17.3"]
|
||||
manimgl = ["manimgl>=1.6.1"]
|
||||
pyqt6 = ["pyqt6>=6.6.1"]
|
||||
manim = ["manim>=0.18.0"]
|
||||
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
|
||||
pyqt6 = ["pyqt6>=6.7.0"]
|
||||
pyqt6-full = ["manim-slides[full,pyqt6]"]
|
||||
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
|
||||
pyside6 = ["pyside6>=6.6.1"]
|
||||
pyside6-full = ["manim-slides[full,pyside6]"]
|
||||
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
||||
tests = [
|
||||
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
|
||||
"pytest>=7.4.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-env>=0.8.2",
|
||||
"pytest-missing-modules>=0.1.0",
|
||||
"pytest-qt>=4.2.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
manim-slides = "manim_slides.__main__:cli"
|
||||
@ -77,6 +83,62 @@ Founding = "https://github.com/sponsors/jeertmans"
|
||||
Homepage = "https://github.com/jeertmans/manim-slides"
|
||||
Repository = "https://github.com/jeertmans/manim-slides"
|
||||
|
||||
[tool.bumpversion]
|
||||
allow_dirty = false
|
||||
commit = true
|
||||
commit_args = ""
|
||||
current_version = "5.1.8"
|
||||
ignore_missing_version = false
|
||||
message = "chore(deps): bump version from {current_version} to {new_version}"
|
||||
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
|
||||
regex = false
|
||||
replace = "{new_version}"
|
||||
search = "{current_version}"
|
||||
serialize = ["{major}.{minor}.{patch}-rc{release}", "{major}.{minor}.{patch}"]
|
||||
sign_tags = false
|
||||
tag = false
|
||||
tag_message = "Bump version: {current_version} → {new_version}"
|
||||
tag_name = "v{new_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "manim_slides/__version__.py"
|
||||
replace = '__version__ = "{new_version}"'
|
||||
search = '__version__ = "{current_version}"'
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CITATION.cff"
|
||||
replace = "version: v{new_version}"
|
||||
search = "version: v{current_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CHANGELOG.md"
|
||||
replace = "v{new_version}"
|
||||
search = "Unreleased"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CHANGELOG.md"
|
||||
replace = "v{new_version}"
|
||||
search = "unreleased"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CHANGELOG.md"
|
||||
replace = "v{current_version}...v{new_version}"
|
||||
search = "v{current_version}...HEAD"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CHANGELOG.md"
|
||||
replace = '''<!-- start changelog -->
|
||||
|
||||
(unreleased)=
|
||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)'''
|
||||
search = "<!-- start changelog -->"
|
||||
|
||||
[tool.codespell]
|
||||
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
|
||||
check-hidden = true
|
||||
ignore-words-list = "master"
|
||||
skip = "uv.lock"
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
@ -86,49 +148,55 @@ exclude_lines = [
|
||||
]
|
||||
precision = 2
|
||||
|
||||
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
||||
content-type = "text/markdown"
|
||||
|
||||
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
||||
text = """<p align="center">
|
||||
<a href="https://www.github.com/jeertmans/manin-slides">
|
||||
<img src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png"/>
|
||||
</a>
|
||||
</p>
|
||||
"""
|
||||
|
||||
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
||||
path = "README.md"
|
||||
start-after = "<!-- start pypi -->"
|
||||
|
||||
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
|
||||
pattern = '> \[!([A-Z]+)\]'
|
||||
replacement = '> **\1:**'
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "manim_slides/__version__.py"
|
||||
|
||||
[tool.mypy]
|
||||
disallow_untyped_decorators = false
|
||||
install_types = true
|
||||
python_version = "3.8"
|
||||
python_version = "3.9"
|
||||
strict = true
|
||||
|
||||
[tool.pdm.dev-dependencies]
|
||||
dev = [
|
||||
"bump2version>=1.0.1",
|
||||
"pre-commit>=3.5.0",
|
||||
]
|
||||
github-action = ["setuptools"]
|
||||
test = [
|
||||
"manim-slides[manim,manimgl,pyqt6]",
|
||||
"pytest>=7.4.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-env>=0.8.2",
|
||||
"pytest-qt>=4.2.0",
|
||||
"pytest-xdist>=3.3.1",
|
||||
]
|
||||
|
||||
[tool.pdm.resolution.overrides]
|
||||
manimpango = "<1.0.0,>=0.5.0" # This conflicts with ManimGL, hopefully not an issue
|
||||
skia-pathops = "0.8.0.post1" # From manim 0.18.0 (Python 3.12 support)
|
||||
|
||||
[tool.pdm.version]
|
||||
path = "manim_slides/__version__.py"
|
||||
source = "file"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [
|
||||
"--cov-report=xml",
|
||||
"--cov=manim_slides",
|
||||
]
|
||||
env = [
|
||||
"QT_API=pyside6",
|
||||
"QT_QPA_PLATFORM=offscreen",
|
||||
]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
'''ignore:'audioop' is deprecated:DeprecationWarning''',
|
||||
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
|
||||
'ignore::DeprecationWarning:pkg_resources.*:',
|
||||
'ignore::DeprecationWarning:pydub.*:',
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
extend-exclude = ["manim_slides/resources.py"]
|
||||
extend-include = ["*.ipynb"]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
target-version = "py39"
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-ignore = [
|
||||
@ -147,3 +215,20 @@ extend-ignore = [
|
||||
]
|
||||
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
|
||||
isort = {known-first-party = ["manim_slides", "tests"]}
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"docs/source/reference/magic_example.ipynb" = ["F403", "F405"]
|
||||
"tests/test_slide.py" = ["N801"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"bump-my-version>=0.20.3",
|
||||
"pre-commit>=3.5.0",
|
||||
"setuptools>=73.0.1",
|
||||
]
|
||||
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'",
|
||||
]
|
||||
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -51,3 +51,28 @@ class ManimSlidesLogo(Scene):
|
||||
) # order matters
|
||||
logo.move_to(ORIGIN)
|
||||
self.add(logo)
|
||||
|
||||
|
||||
class ManimSlidesFavicon(Scene):
|
||||
def construct(self):
|
||||
tex_template = TexTemplate()
|
||||
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
|
||||
fill_color = "#c9d1d9"
|
||||
stroke_color = "#343434"
|
||||
play = Tex(
|
||||
r"\faStepBackward\faStepForward",
|
||||
fill_color=fill_color,
|
||||
stroke_color=stroke_color,
|
||||
tex_template=tex_template,
|
||||
).scale(4)
|
||||
comment = Tex(
|
||||
r"\reflectbox{\faComment*[regular]}",
|
||||
fill_color=fill_color,
|
||||
stroke_color=stroke_color,
|
||||
tex_template=tex_template,
|
||||
).scale(9)
|
||||
comment.move_to(play)
|
||||
comment.shift(0.4 * DOWN)
|
||||
favicon = VGroup(comment, play).scale(3)
|
||||
favicon.move_to(ORIGIN)
|
||||
self.add(favicon)
|
||||
|
5
static/make_favicon.sh
Executable file
5
static/make_favicon.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#! /bin/bash
|
||||
|
||||
uv run manim-slides render -t -qk -s --format png --resolution 64,64 static/logo.py ManimSlidesFavicon && mv media/images/logo/*.png static/favicon.png
|
||||
|
||||
ln -f -r -s static/favicon.png docs/source/_static/favicon.png
|
@ -1,21 +1,21 @@
|
||||
#! /bin/bash
|
||||
|
||||
MANIM_SLIDES_THEME=light pdm run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo.png
|
||||
MANIM_SLIDES_THEME=light uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo.png
|
||||
|
||||
ln -f -r -s static/logo.png docs/source/_static/logo.png
|
||||
|
||||
MANIM_SLIDES_THEME=dark_docs pdm run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_docs.png
|
||||
MANIM_SLIDES_THEME=dark_docs uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_docs.png
|
||||
|
||||
ln -f -r -s static/logo_dark_docs.png docs/source/_static/logo_dark_docs.png
|
||||
|
||||
MANIM_SLIDES_THEME=dark_github pdm run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_github.png
|
||||
MANIM_SLIDES_THEME=dark_github uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_github.png
|
||||
|
||||
ln -f -r -s static/logo_dark_github.png docs/source/_static/logo_dark_github.png
|
||||
|
||||
MANIM_SLIDES_THEME=light pdm run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_light_transparent.png
|
||||
MANIM_SLIDES_THEME=light uv run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_light_transparent.png
|
||||
|
||||
ln -f -r -s static/logo_light_transparent.png docs/source/_static/logo_light_transparent.png
|
||||
|
||||
MANIM_SLIDES_THEME=dark_docs pdm run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_transparent.png
|
||||
MANIM_SLIDES_THEME=dark_docs uv run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_transparent.png
|
||||
|
||||
ln -f -r -s static/logo_dark_transparent.png docs/source/_static/logo_dark_transparent.png
|
||||
|
@ -1,7 +1,7 @@
|
||||
import random
|
||||
import string
|
||||
from collections.abc import Generator, Iterator
|
||||
from pathlib import Path
|
||||
from typing import Generator, Iterator, List
|
||||
|
||||
import pytest
|
||||
|
||||
@ -65,14 +65,26 @@ def random_path(
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def paths() -> Generator[List[Path], None, None]:
|
||||
def paths() -> Generator[list[Path], None, None]:
|
||||
random.seed(1234)
|
||||
|
||||
yield [random_path() for _ in range(20)]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture
|
||||
def presentation_config(
|
||||
slides_folder: Path,
|
||||
) -> Generator[PresentationConfig, None, None]:
|
||||
yield PresentationConfig.from_file(slides_folder / "BasicSlide.json")
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||
"""Make sure missing modules run at the very end."""
|
||||
|
||||
def uses_missing_modules_fixtures(item: pytest.Item) -> int:
|
||||
if "missing_modules" in getattr(item, "fixturenames", []):
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
items.sort(key=uses_missing_modules_fixtures)
|
||||
|
@ -38,3 +38,7 @@ class BasicSlide(Slide):
|
||||
|
||||
self.next_slide()
|
||||
self.zoom(other_text, [])
|
||||
|
||||
|
||||
class BasicSlideSkipReversing(BasicSlide):
|
||||
skip_reversing = True
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import MutableMapping
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
import pytest
|
||||
|
||||
|
66
tests/test_checkhealth.py
Normal file
66
tests/test_checkhealth.py
Normal file
@ -0,0 +1,66 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from itertools import chain, combinations
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from pytest_missing_modules.plugin import MissingModulesContextGenerator
|
||||
|
||||
from manim_slides.__version__ import __version__
|
||||
from manim_slides.checkhealth import checkhealth
|
||||
|
||||
MANIM_NOT_INSTALLED = importlib.util.find_spec("manim") is None
|
||||
MANIMGL_NOT_INSTALLED = importlib.util.find_spec("manimlib") is None
|
||||
PYQT6_NOT_INSTALLED = importlib.util.find_spec("PyQt6") is None
|
||||
PYSIDE6_NOT_INSTALLED = importlib.util.find_spec("PySide6") is None
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Selected binding 'pyqt6' could not be found")
|
||||
@pytest.mark.parametrize(
|
||||
"names",
|
||||
list(
|
||||
chain.from_iterable(
|
||||
combinations(("manim", "manimlib", "PyQt6", "PySide6"), r=r)
|
||||
for r in range(0, 5)
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_checkhealth(
|
||||
names: tuple[str, ...], missing_modules: MissingModulesContextGenerator
|
||||
) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
manim_missing = "manim" in names or MANIM_NOT_INSTALLED
|
||||
manimlib_missing = "manimlib" in names or MANIMGL_NOT_INSTALLED
|
||||
pyqt6_missing = "PyQt6" in names or PYQT6_NOT_INSTALLED
|
||||
pyside6_missing = "PySide6" in names or PYSIDE6_NOT_INSTALLED
|
||||
|
||||
if "qtpy" in sys.modules:
|
||||
del sys.modules["qtpy"] # Avoid using cached module
|
||||
|
||||
with missing_modules(*names):
|
||||
result = runner.invoke(
|
||||
checkhealth,
|
||||
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert f"Manim Slides version: {__version__}" in result.output
|
||||
assert sys.executable in result.output
|
||||
|
||||
if manim_missing:
|
||||
assert "manim not found" in result.output
|
||||
else:
|
||||
assert "manim (version:" in result.output
|
||||
|
||||
if manimlib_missing:
|
||||
assert "manimgl not found" in result.output
|
||||
else:
|
||||
assert "manimgl (version:" in result.output
|
||||
|
||||
if pyqt6_missing and pyside6_missing:
|
||||
assert "No Qt API found" in result.output
|
||||
elif pyqt6_missing:
|
||||
assert "Qt API: pyside6 (version:" in result.output
|
||||
else:
|
||||
assert "Qt API: pyqt6 (version:" in result.output
|
@ -1,3 +1,4 @@
|
||||
import shutil
|
||||
from enum import EnumMeta
|
||||
from pathlib import Path
|
||||
|
||||
@ -17,6 +18,7 @@ from manim_slides.convert import (
|
||||
ControlsLayout,
|
||||
Converter,
|
||||
Display,
|
||||
HtmlZip,
|
||||
JsBool,
|
||||
JsFalse,
|
||||
JsNull,
|
||||
@ -138,7 +140,8 @@ def test_unquoted_enum(enum_type: EnumMeta) -> None:
|
||||
|
||||
class TestConverter:
|
||||
@pytest.mark.parametrize(
|
||||
("name", "converter"), [("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint)]
|
||||
("name", "converter"),
|
||||
[("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint), ("zip", HtmlZip)],
|
||||
)
|
||||
def test_from_string(self, name: str, converter: type) -> None:
|
||||
assert Converter.from_string(name) == converter
|
||||
@ -150,14 +153,59 @@ class TestConverter:
|
||||
RevealJS(presentation_configs=[presentation_config]).convert_to(out_file)
|
||||
assert out_file.exists()
|
||||
assert Path(tmp_path / "slides_assets").is_dir()
|
||||
file_contents = Path(out_file).read_text()
|
||||
file_contents = out_file.read_text()
|
||||
assert "manim" in file_contents.casefold()
|
||||
|
||||
def test_pdf_converter(
|
||||
def test_htmlzip_converter(
|
||||
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||
) -> None:
|
||||
archive = tmp_path / "got.zip"
|
||||
expected = tmp_path / "expected.html"
|
||||
got = tmp_path / "got.html"
|
||||
|
||||
HtmlZip(presentation_configs=[presentation_config]).convert_to(archive)
|
||||
RevealJS(presentation_configs=[presentation_config]).convert_to(expected)
|
||||
|
||||
shutil.unpack_archive(str(archive), extract_dir=tmp_path)
|
||||
|
||||
assert archive.exists()
|
||||
assert got.exists()
|
||||
assert expected.exists()
|
||||
|
||||
assert got.read_text() == expected.read_text().replace(
|
||||
"expected_assets", "got_assets"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("num_presentation_configs", (1, 2))
|
||||
def test_revealjs_multiple_scenes_converter(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
presentation_config: PresentationConfig,
|
||||
num_presentation_configs: int,
|
||||
) -> None:
|
||||
out_file = tmp_path / "slides.html"
|
||||
RevealJS(
|
||||
presentation_configs=[
|
||||
presentation_config for _ in range(num_presentation_configs)
|
||||
]
|
||||
).convert_to(out_file)
|
||||
assert out_file.exists()
|
||||
assets_dir = Path(tmp_path / "slides_assets")
|
||||
assert assets_dir.is_dir()
|
||||
|
||||
got = sum(1 for _ in assets_dir.iterdir())
|
||||
expected = num_presentation_configs * len(presentation_config.slides)
|
||||
|
||||
assert got == expected
|
||||
|
||||
@pytest.mark.parametrize("frame_index", ("first", "last"))
|
||||
def test_pdf_converter(
|
||||
self, frame_index: str, tmp_path: Path, presentation_config: PresentationConfig
|
||||
) -> None:
|
||||
out_file = tmp_path / "slides.pdf"
|
||||
PDF(presentation_configs=[presentation_config]).convert_to(out_file)
|
||||
PDF(
|
||||
presentation_configs=[presentation_config], frame_index=frame_index
|
||||
).convert_to(out_file)
|
||||
assert out_file.exists()
|
||||
|
||||
def test_converter_no_presentation_config(self) -> None:
|
||||
|
@ -21,6 +21,7 @@ def assert_import(
|
||||
|
||||
|
||||
def test_force_api() -> None:
|
||||
pytest.importorskip("manimlib")
|
||||
import manim # noqa: F401
|
||||
|
||||
if "manimlib" in sys.modules:
|
||||
@ -54,6 +55,7 @@ def test_invalid_api() -> None:
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:assert_import")
|
||||
def test_manim_and_manimgl_imported() -> None:
|
||||
pytest.importorskip("manimlib")
|
||||
import manim # noqa: F401
|
||||
import manimlib # noqa: F401
|
||||
|
||||
@ -78,6 +80,7 @@ def test_manim_imported() -> None:
|
||||
|
||||
|
||||
def test_manimgl_imported() -> None:
|
||||
pytest.importorskip("manimlib")
|
||||
import manimlib # noqa: F401
|
||||
|
||||
if "manim" in sys.modules:
|
||||
|
@ -1,5 +1,5 @@
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Tuple
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
@ -20,11 +20,11 @@ def auto_shutdown_qapp() -> Iterator[None]:
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def args(slides_folder: Path) -> Iterator[Tuple[str, ...]]:
|
||||
def args(slides_folder: Path) -> Iterator[tuple[str, ...]]:
|
||||
yield ("--folder", str(slides_folder), "--skip-all", "--playback-rate", "25")
|
||||
|
||||
|
||||
def test_present(args: Tuple[str, ...]) -> None:
|
||||
def test_present(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -34,7 +34,7 @@ def test_present(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_unexisting_slide(args: Tuple[str, ...]) -> None:
|
||||
def test_present_unexisting_slide(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -44,7 +44,7 @@ def test_present_unexisting_slide(args: Tuple[str, ...]) -> None:
|
||||
assert "UnexistingSlide.json does not exist" in results.stdout
|
||||
|
||||
|
||||
def test_present_full_screen(args: Tuple[str, ...]) -> None:
|
||||
def test_present_full_screen(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -54,7 +54,7 @@ def test_present_full_screen(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_hide_mouse(args: Tuple[str, ...]) -> None:
|
||||
def test_present_hide_mouse(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -64,7 +64,7 @@ def test_present_hide_mouse(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_ignore_aspect_ratio(args: Tuple[str, ...]) -> None:
|
||||
def test_present_ignore_aspect_ratio(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -76,7 +76,7 @@ def test_present_ignore_aspect_ratio(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_start_at(args: Tuple[str, ...]) -> None:
|
||||
def test_present_start_at(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -86,7 +86,7 @@ def test_present_start_at(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_start_at_invalid(args: Tuple[str, ...]) -> None:
|
||||
def test_present_start_at_invalid(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -96,7 +96,7 @@ def test_present_start_at_invalid(args: Tuple[str, ...]) -> None:
|
||||
assert "Could not set presentation index to 1234"
|
||||
|
||||
|
||||
def test_present_start_at_scene_number(args: Tuple[str, ...]) -> None:
|
||||
def test_present_start_at_scene_number(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -108,7 +108,7 @@ def test_present_start_at_scene_number(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_start_at_slide_number(args: Tuple[str, ...]) -> None:
|
||||
def test_present_start_at_slide_number(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -120,7 +120,7 @@ def test_present_start_at_slide_number(args: Tuple[str, ...]) -> None:
|
||||
assert results.stdout == ""
|
||||
|
||||
|
||||
def test_present_set_screen(args: Tuple[str, ...]) -> None:
|
||||
def test_present_set_screen(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
@ -131,7 +131,7 @@ def test_present_set_screen(args: Tuple[str, ...]) -> None:
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Fails when running the whole test suite.")
|
||||
def test_present_set_invalid_screen(args: Tuple[str, ...]) -> None:
|
||||
def test_present_set_invalid_screen(args: tuple[str, ...]) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
|
@ -1,8 +1,9 @@
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from manim import (
|
||||
@ -18,13 +19,30 @@ from manim import (
|
||||
GrowFromCenter,
|
||||
Text,
|
||||
)
|
||||
from packaging import version
|
||||
from pydantic import ValidationError
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
from manim_slides.config import PresentationConfig
|
||||
from manim_slides.defaults import FOLDER_PATH
|
||||
from manim_slides.render import render
|
||||
from manim_slides.slide.manim import Slide
|
||||
from manim_slides.slide.manim import Slide as CESlide
|
||||
|
||||
|
||||
class CEGLSlide(CESlide):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
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(
|
||||
@ -34,8 +52,8 @@ from manim_slides.slide.manim import Slide
|
||||
pytest.param(
|
||||
"--GL",
|
||||
marks=pytest.mark.skipif(
|
||||
version.parse(np.__version__) >= version.parse("1.25"),
|
||||
reason="ManimGL requires numpy<1.25, which is outdate",
|
||||
sys.version_info >= (3, 12),
|
||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -79,261 +97,417 @@ def test_render_basic_slide(
|
||||
assert local_presentation_config.resolution == presentation_config.resolution
|
||||
|
||||
|
||||
def assert_constructs(cls: type) -> type:
|
||||
class Wrapper:
|
||||
@classmethod
|
||||
def test_construct(_) -> None: # noqa: N804
|
||||
cls().construct()
|
||||
def test_clear_cache(
|
||||
slides_file: Path,
|
||||
) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
return Wrapper
|
||||
with runner.isolated_filesystem() as tmp_dir:
|
||||
local_media_folder = (
|
||||
Path(tmp_dir)
|
||||
/ "media"
|
||||
/ "videos"
|
||||
/ slides_file.stem
|
||||
/ "480p15"
|
||||
/ "partial_movie_files"
|
||||
/ "BasicSlide"
|
||||
)
|
||||
local_slides_folder = Path(tmp_dir) / "slides"
|
||||
|
||||
assert not local_media_folder.exists()
|
||||
assert not local_slides_folder.exists()
|
||||
results = runner.invoke(render, [str(slides_file), "BasicSlide", "-ql"])
|
||||
|
||||
assert results.exit_code == 0, results
|
||||
assert local_media_folder.is_dir() and list(local_media_folder.iterdir())
|
||||
assert local_slides_folder.exists()
|
||||
|
||||
results = runner.invoke(
|
||||
render, [str(slides_file), "BasicSlide", "-ql", "--flush_cache"]
|
||||
)
|
||||
|
||||
assert results.exit_code == 0, results
|
||||
assert local_media_folder.is_dir() and not list(local_media_folder.iterdir())
|
||||
assert local_slides_folder.exists()
|
||||
|
||||
results = runner.invoke(
|
||||
render, [str(slides_file), "BasicSlide", "-ql", "--disable_caching"]
|
||||
)
|
||||
|
||||
assert results.exit_code == 0, results
|
||||
assert local_media_folder.is_dir() and list(local_media_folder.iterdir())
|
||||
assert local_slides_folder.exists()
|
||||
|
||||
results = runner.invoke(
|
||||
render,
|
||||
[
|
||||
str(slides_file),
|
||||
"BasicSlide",
|
||||
"-ql",
|
||||
"--disable_caching",
|
||||
"--flush_cache",
|
||||
],
|
||||
)
|
||||
|
||||
assert results.exit_code == 0, results
|
||||
assert local_media_folder.is_dir() and not list(local_media_folder.iterdir())
|
||||
assert local_slides_folder.exists()
|
||||
|
||||
|
||||
def assert_renders(cls: type) -> type:
|
||||
class Wrapper:
|
||||
@classmethod
|
||||
def test_render(_) -> None: # noqa: N804
|
||||
cls().render()
|
||||
@pytest.mark.parametrize(
|
||||
"renderer",
|
||||
[
|
||||
"--CE",
|
||||
pytest.param(
|
||||
"--GL",
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 12),
|
||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("klass", "skip_reversing"),
|
||||
[("BasicSlide", False), ("BasicSlideSkipReversing", True)],
|
||||
)
|
||||
def test_skip_reversing(
|
||||
renderer: str,
|
||||
slides_file: Path,
|
||||
manimgl_config: Path,
|
||||
klass: str,
|
||||
skip_reversing: bool,
|
||||
) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
return Wrapper
|
||||
with runner.isolated_filesystem() as tmp_dir:
|
||||
shutil.copy(manimgl_config, tmp_dir)
|
||||
results = runner.invoke(render, [renderer, str(slides_file), klass, "-ql"])
|
||||
|
||||
assert results.exit_code == 0, results
|
||||
|
||||
local_slides_folder = (Path(tmp_dir) / "slides").resolve(strict=True)
|
||||
|
||||
local_config_file = (local_slides_folder / f"{klass}.json").resolve(strict=True)
|
||||
|
||||
local_presentation_config = PresentationConfig.from_file(local_config_file)
|
||||
|
||||
for slide in local_presentation_config.slides:
|
||||
if skip_reversing:
|
||||
assert slide.file == slide.rev_file
|
||||
else:
|
||||
assert slide.file != slide.rev_file
|
||||
|
||||
|
||||
def init_slide(cls: SlideType) -> Slide:
|
||||
if issubclass(cls, CESlide):
|
||||
return cls()
|
||||
elif issubclass(cls, GLSlide):
|
||||
from manimlib.config import get_configuration, parse_cli
|
||||
from manimlib.extract_scene import get_scene_config
|
||||
|
||||
args = parse_cli()
|
||||
config = get_configuration(args)
|
||||
scene_config = get_scene_config(config)
|
||||
return cls(**scene_config)
|
||||
|
||||
raise ValueError(f"Unsupported class {cls}")
|
||||
|
||||
|
||||
parametrize_base_cls = pytest.mark.parametrize(
|
||||
"base_cls", (CESlide, GLSlide, CEGLSlide), ids=("CE", "GL", "CE(GL)")
|
||||
)
|
||||
|
||||
|
||||
def assert_constructs(cls: SlideType) -> None:
|
||||
init_slide(cls).construct()
|
||||
|
||||
|
||||
def assert_renders(cls: SlideType) -> None:
|
||||
init_slide(cls).render()
|
||||
|
||||
|
||||
class TestSlide:
|
||||
@assert_constructs
|
||||
class TestDefaultProperties(Slide):
|
||||
def construct(self) -> None:
|
||||
assert self._output_folder == FOLDER_PATH
|
||||
assert len(self._slides) == 0
|
||||
assert self._current_slide == 1
|
||||
assert self._start_animation == 0
|
||||
assert len(self._canvas) == 0
|
||||
assert self._wait_time_between_slides == 0.0
|
||||
def test_default_properties(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
assert self._output_folder == FOLDER_PATH
|
||||
assert len(self._slides) == 0
|
||||
assert self._current_slide == 1
|
||||
assert self._start_animation == 0
|
||||
assert len(self._canvas) == 0
|
||||
assert self._wait_time_between_slides == 0.0
|
||||
|
||||
@assert_renders
|
||||
class TestMultipleAnimationsInLastSlide(Slide):
|
||||
"""Check against solution for issue #161."""
|
||||
@parametrize_base_cls
|
||||
def test_frame_height(self, base_cls: SlideType) -> None:
|
||||
@assert_constructs
|
||||
class _(base_cls): # type: ignore
|
||||
def construct(self) -> None:
|
||||
assert self._frame_height > 0 and isinstance(self._frame_height, float)
|
||||
|
||||
def construct(self) -> None:
|
||||
circle = Circle(color=BLUE)
|
||||
dot = Dot()
|
||||
@parametrize_base_cls
|
||||
def test_frame_width(self, base_cls: SlideType) -> None:
|
||||
@assert_constructs
|
||||
class _(base_cls): # type: ignore
|
||||
def construct(self) -> None:
|
||||
assert self._frame_width > 0 and isinstance(self._frame_width, float)
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.play(FadeIn(dot))
|
||||
self.next_slide()
|
||||
@parametrize_base_cls
|
||||
def test_resolution(self, base_cls: SlideType) -> None:
|
||||
@assert_constructs
|
||||
class _(base_cls): # type: ignore
|
||||
def construct(self) -> None:
|
||||
pw, ph = self._resolution
|
||||
assert isinstance(pw, int) and pw > 0
|
||||
assert isinstance(ph, int) and ph > 0
|
||||
|
||||
self.play(dot.animate.move_to(RIGHT))
|
||||
self.play(dot.animate.move_to(UP))
|
||||
self.play(dot.animate.move_to(LEFT))
|
||||
self.play(dot.animate.move_to(DOWN))
|
||||
@parametrize_base_cls
|
||||
def test_backround_color(self, base_cls: SlideType) -> None:
|
||||
@assert_constructs
|
||||
class _(base_cls): # type: ignore
|
||||
def construct(self) -> None:
|
||||
assert self._background_color in ["#000000", "#000"] # DEFAULT
|
||||
|
||||
@assert_renders
|
||||
class TestFileTooLong(Slide):
|
||||
"""Check against solution for issue #123."""
|
||||
def test_multiple_animations_in_last_slide(self) -> None:
|
||||
@assert_renders
|
||||
class _(CESlide):
|
||||
"""Check against solution for issue #161."""
|
||||
|
||||
def construct(self) -> None:
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
dot = Dot()
|
||||
self.play(GrowFromCenter(circle), run_time=0.1)
|
||||
def construct(self) -> None:
|
||||
circle = Circle(color=BLUE)
|
||||
dot = Dot()
|
||||
|
||||
for _ in range(30):
|
||||
direction = (random.random() - 0.5) * LEFT + (
|
||||
random.random() - 0.5
|
||||
) * UP
|
||||
self.play(dot.animate.move_to(direction), run_time=0.1)
|
||||
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
|
||||
|
||||
@assert_constructs
|
||||
class TestLoop(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
self.add(text)
|
||||
|
||||
assert not self._base_slide_config.loop
|
||||
|
||||
self.next_slide(loop=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
assert self._base_slide_config.loop
|
||||
|
||||
self.next_slide(loop=False)
|
||||
|
||||
assert not self._base_slide_config.loop
|
||||
|
||||
@assert_constructs
|
||||
class TestAutoNext(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
self.add(text)
|
||||
|
||||
assert not self._base_slide_config.auto_next
|
||||
|
||||
self.next_slide(auto_next=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
assert self._base_slide_config.auto_next
|
||||
|
||||
self.next_slide(auto_next=False)
|
||||
|
||||
assert not self._base_slide_config.auto_next
|
||||
|
||||
@assert_constructs
|
||||
class TestLoopAndAutoNextFails(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
self.add(text)
|
||||
|
||||
self.next_slide(loop=True, auto_next=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.play(FadeIn(dot))
|
||||
self.next_slide()
|
||||
|
||||
@assert_constructs
|
||||
class TestPlaybackRate(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
self.play(dot.animate.move_to(RIGHT))
|
||||
self.play(dot.animate.move_to(UP))
|
||||
self.play(dot.animate.move_to(LEFT))
|
||||
self.play(dot.animate.move_to(DOWN))
|
||||
|
||||
self.add(text)
|
||||
def test_file_too_long(self) -> None:
|
||||
@assert_renders
|
||||
class _(CESlide):
|
||||
"""Check against solution for issue #123."""
|
||||
|
||||
assert self._base_slide_config.playback_rate == 1.0
|
||||
def construct(self) -> None:
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
dot = Dot()
|
||||
self.play(GrowFromCenter(circle), run_time=0.1)
|
||||
|
||||
self.next_slide(playback_rate=2.0)
|
||||
self.play(text.animate.scale(2))
|
||||
for _ in range(30):
|
||||
direction = (random.random() - 0.5) * LEFT + (
|
||||
random.random() - 0.5
|
||||
) * UP
|
||||
self.play(dot.animate.move_to(direction), run_time=0.1)
|
||||
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
|
||||
|
||||
assert self._base_slide_config.playback_rate == 2.0
|
||||
def test_loop(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
@assert_constructs
|
||||
class TestReversedPlaybackRate(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
self.add(text)
|
||||
|
||||
self.add(text)
|
||||
assert not self._base_slide_config.loop
|
||||
|
||||
assert self._base_slide_config.reversed_playback_rate == 1.0
|
||||
self.next_slide(loop=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
self.next_slide(reversed_playback_rate=2.0)
|
||||
self.play(text.animate.scale(2))
|
||||
assert self._base_slide_config.loop
|
||||
|
||||
assert self._base_slide_config.reversed_playback_rate == 2.0
|
||||
self.next_slide(loop=False)
|
||||
|
||||
@assert_constructs
|
||||
class TestNotes(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
assert not self._base_slide_config.loop
|
||||
|
||||
self.add(text)
|
||||
def test_auto_next(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
assert self._base_slide_config.notes == ""
|
||||
self.add(text)
|
||||
|
||||
self.next_slide(notes="test")
|
||||
self.play(text.animate.scale(2))
|
||||
assert not self._base_slide_config.auto_next
|
||||
|
||||
assert self._base_slide_config.notes == "test"
|
||||
self.next_slide(auto_next=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
@assert_constructs
|
||||
class TestWipe(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
bye = Text("Bye")
|
||||
assert self._base_slide_config.auto_next
|
||||
|
||||
self.add(text)
|
||||
self.next_slide(auto_next=False)
|
||||
|
||||
assert text in self.mobjects
|
||||
assert bye not in self.mobjects
|
||||
assert not self._base_slide_config.auto_next
|
||||
|
||||
self.wipe([text], [bye])
|
||||
def test_loop_and_auto_next_succeeds(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
assert text not in self.mobjects
|
||||
assert bye in self.mobjects
|
||||
self.add(text)
|
||||
|
||||
@assert_constructs
|
||||
class TestZoom(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
bye = Text("Bye")
|
||||
self.next_slide(loop=True, auto_next=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
self.add(text)
|
||||
self.next_slide()
|
||||
|
||||
assert text in self.mobjects
|
||||
assert bye not in self.mobjects
|
||||
def test_playback_rate(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
self.zoom([text], [bye])
|
||||
self.add(text)
|
||||
|
||||
assert text not in self.mobjects
|
||||
assert bye in self.mobjects
|
||||
assert self._base_slide_config.playback_rate == 1.0
|
||||
|
||||
@assert_constructs
|
||||
class TestPlay(Slide):
|
||||
def construct(self) -> None:
|
||||
assert self._current_animation == 0
|
||||
circle = Circle(color=BLUE)
|
||||
dot = Dot()
|
||||
self.next_slide(playback_rate=2.0)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
assert self._current_animation == 1
|
||||
self.play(FadeIn(dot))
|
||||
assert self._current_animation == 2
|
||||
assert self._base_slide_config.playback_rate == 2.0
|
||||
|
||||
@assert_constructs
|
||||
class TestWaitTimeBetweenSlides(Slide):
|
||||
def construct(self) -> None:
|
||||
self._wait_time_between_slides = 1.0
|
||||
assert self._current_animation == 0
|
||||
circle = Circle(color=BLUE)
|
||||
self.play(GrowFromCenter(circle))
|
||||
assert self._current_animation == 1
|
||||
self.next_slide()
|
||||
assert self._current_animation == 2 # self.wait = +1
|
||||
def test_reversed_playback_rate(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
@assert_constructs
|
||||
class TestNextSlide(Slide):
|
||||
def construct(self) -> None:
|
||||
assert self._current_slide == 1
|
||||
self.next_slide()
|
||||
assert self._current_slide == 1
|
||||
circle = Circle(color=BLUE)
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.next_slide()
|
||||
assert self._current_slide == 2
|
||||
self.next_slide()
|
||||
assert self._current_slide == 2
|
||||
self.add(text)
|
||||
|
||||
@assert_constructs
|
||||
class TestCanvas(Slide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
bye = Text("Bye")
|
||||
assert self._base_slide_config.reversed_playback_rate == 1.0
|
||||
|
||||
assert len(self.canvas) == 0
|
||||
self.next_slide(reversed_playback_rate=2.0)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
self.add(text)
|
||||
assert self._base_slide_config.reversed_playback_rate == 2.0
|
||||
|
||||
assert len(self.canvas) == 0
|
||||
def test_notes(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
|
||||
self.add_to_canvas(text=text)
|
||||
self.add(text)
|
||||
|
||||
assert len(self.canvas) == 1
|
||||
assert self._base_slide_config.notes == ""
|
||||
|
||||
self.add(bye)
|
||||
self.next_slide(notes="test")
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
assert len(self.canvas) == 1
|
||||
assert self._base_slide_config.notes == "test"
|
||||
|
||||
assert text not in self.mobjects_without_canvas
|
||||
assert bye in self.mobjects_without_canvas
|
||||
def test_wipe(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
bye = Text("Bye")
|
||||
|
||||
self.remove(text)
|
||||
self.add(text)
|
||||
|
||||
assert len(self.canvas) == 1
|
||||
assert text in self.mobjects
|
||||
assert bye not in self.mobjects
|
||||
|
||||
self.add_to_canvas(bye=bye)
|
||||
self.wipe([text], [bye])
|
||||
|
||||
assert len(self.canvas) == 2
|
||||
assert text not in self.mobjects
|
||||
assert bye in self.mobjects
|
||||
|
||||
self.remove_from_canvas("text", "bye")
|
||||
def test_zoom(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
bye = Text("Bye")
|
||||
|
||||
assert len(self.canvas) == 0
|
||||
self.add(text)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
self.remove_from_canvas("text")
|
||||
assert text in self.mobjects
|
||||
assert bye not in self.mobjects
|
||||
|
||||
self.zoom([text], [bye])
|
||||
|
||||
assert text not in self.mobjects
|
||||
assert bye in self.mobjects
|
||||
|
||||
def test_animation_count(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
assert self._current_animation == 0
|
||||
circle = Circle(color=BLUE)
|
||||
dot = Dot()
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
assert self._current_animation == 1
|
||||
self.play(FadeIn(dot))
|
||||
assert self._current_animation == 2
|
||||
|
||||
def test_wait_time_between_slides(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
self._wait_time_between_slides = 1.0
|
||||
assert self._current_animation == 0
|
||||
circle = Circle(color=BLUE)
|
||||
self.play(GrowFromCenter(circle))
|
||||
assert self._current_animation == 1
|
||||
self.next_slide()
|
||||
assert self._current_animation == 2 # self.wait = +1
|
||||
|
||||
def test_next_slide(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
assert self._current_slide == 1
|
||||
self.next_slide()
|
||||
assert self._current_slide == 1
|
||||
circle = Circle(color=BLUE)
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.next_slide()
|
||||
assert self._current_slide == 2
|
||||
self.next_slide()
|
||||
assert self._current_slide == 2
|
||||
|
||||
def test_canvas(self) -> None:
|
||||
@assert_constructs
|
||||
class _(CESlide):
|
||||
def construct(self) -> None:
|
||||
text = Text("Some text")
|
||||
bye = Text("Bye")
|
||||
|
||||
assert len(self.canvas) == 0
|
||||
|
||||
self.add(text)
|
||||
|
||||
assert len(self.canvas) == 0
|
||||
|
||||
self.add_to_canvas(text=text)
|
||||
|
||||
assert len(self.canvas) == 1
|
||||
|
||||
self.add(bye)
|
||||
|
||||
assert len(self.canvas) == 1
|
||||
|
||||
assert text not in self.mobjects_without_canvas
|
||||
assert bye in self.mobjects_without_canvas
|
||||
|
||||
self.remove(text)
|
||||
|
||||
assert len(self.canvas) == 1
|
||||
|
||||
self.add_to_canvas(bye=bye)
|
||||
|
||||
assert len(self.canvas) == 2
|
||||
|
||||
self.remove_from_canvas("text", "bye")
|
||||
|
||||
assert len(self.canvas) == 0
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
self.remove_from_canvas("text")
|
||||
|
@ -1,17 +1,16 @@
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from manim_slides.utils import merge_basenames
|
||||
|
||||
|
||||
def test_merge_basenames(paths: List[Path]) -> None:
|
||||
def test_merge_basenames(paths: list[Path]) -> None:
|
||||
path = merge_basenames(paths)
|
||||
assert path.suffix == paths[0].suffix
|
||||
assert path.parent == paths[0].parent
|
||||
|
||||
|
||||
def test_merge_basenames_same_with_different_parent_directories(
|
||||
paths: List[Path],
|
||||
paths: list[Path],
|
||||
) -> None:
|
||||
d1 = Path("a/b/c")
|
||||
d2 = Path("d/e/f")
|
||||
|
@ -69,7 +69,7 @@ class TestWizard:
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "exec", exec_patched)
|
||||
|
||||
for i, (key, _) in enumerate(wizard.config.keys.dict().items()):
|
||||
for i, (key, _) in enumerate(wizard.config.keys.model_dump().items()):
|
||||
open_dialog(i, getattr(wizard.config.keys, key))
|
||||
|
||||
wizard.button_box.accepted.emit()
|
||||
@ -89,7 +89,7 @@ def test_init() -> None:
|
||||
|
||||
assert results.exit_code == 0
|
||||
assert CONFIG_PATH.exists()
|
||||
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
|
||||
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
|
||||
|
||||
|
||||
def test_init_custom_path() -> None:
|
||||
@ -106,7 +106,7 @@ def test_init_custom_path() -> None:
|
||||
assert results.exit_code == 0
|
||||
assert not CONFIG_PATH.exists()
|
||||
assert custom_path.exists()
|
||||
assert Config().dict() == Config.from_file(custom_path).dict()
|
||||
assert Config().model_dump() == Config.from_file(custom_path).model_dump()
|
||||
|
||||
|
||||
def test_init_path_exists() -> None:
|
||||
@ -120,7 +120,7 @@ def test_init_path_exists() -> None:
|
||||
|
||||
assert results.exit_code == 0
|
||||
assert CONFIG_PATH.exists()
|
||||
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
|
||||
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
|
||||
|
||||
results = runner.invoke(init, input="o")
|
||||
|
||||
@ -156,7 +156,7 @@ def test_wizard(monkeypatch: MonkeyPatch) -> None:
|
||||
|
||||
assert results.exit_code == 0
|
||||
assert CONFIG_PATH.exists()
|
||||
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
|
||||
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
|
||||
|
||||
|
||||
def test_wizard_closed_without_saving(monkeypatch: MonkeyPatch) -> None:
|
||||
|
Reference in New Issue
Block a user