mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-07-11 06:02:38 +08:00
Compare commits
123 Commits
v5.1.5
...
tests-grou
Author | SHA1 | Date | |
---|---|---|---|
804cf48a02 | |||
3c6e2db7db | |||
04b0eb5685 | |||
0c6cd67038 | |||
a5412a8df2 | |||
e9480c9bc7 | |||
3e0268a431 | |||
6e14dc9051 | |||
4a400398b8 | |||
d641d2d82c | |||
d3396d3a01 | |||
7a922db6f1 | |||
9b9593985d | |||
c915af19e8 | |||
a8897552d8 | |||
b02dbbcc8c | |||
c6ba210797 | |||
b1212a49d3 | |||
f7ce5fc115 | |||
5f9bbf2a79 | |||
ccbe9d558c | |||
a2bd1ffb67 | |||
e911ec3096 | |||
daf547414c | |||
528952dbc3 | |||
dbced6e62e | |||
941b895083 | |||
289b7c1683 | |||
b07a83898b | |||
074a029759 | |||
b4af76050e | |||
adce58e1b7 | |||
32ab690932 | |||
df31345f83 | |||
ef282300f1 | |||
a9ba1b4fad | |||
4cc6c2865d | |||
0483e2f861 | |||
a58ff6c388 | |||
bf512f2f73 | |||
541b175360 | |||
57ab592d36 | |||
840d1d80d9 | |||
c15cd95565 | |||
478552c528 | |||
1189f37cf3 | |||
e50271b0b2 | |||
98955bee5c | |||
2169938df7 | |||
a207248deb | |||
1c859991b8 | |||
a24915665d | |||
5cce117050 | |||
bd76fbdfd9 | |||
5b026919ea | |||
05c1a16ca3 | |||
3bd8c386b1 | |||
628c8da832 | |||
3b62e6b788 | |||
ff9aac49d7 | |||
16d9d32d33 | |||
988011ff7d | |||
3dbe12b480 | |||
6ba657c0d5 | |||
6c8ab61f9d | |||
91e6e139e3 | |||
d8acbae165 | |||
75af26e601 | |||
a8903b809d | |||
d813aaf313 | |||
d5679924b9 | |||
fb562d88ac | |||
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 |
@ -1,16 +0,0 @@
|
|||||||
[bumpversion]
|
|
||||||
current_version = 5.1.5
|
|
||||||
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
|
name: Bug
|
||||||
description: Report an issue to help improve the project.
|
description: Report an issue to help improve the project.
|
||||||
labels: bug
|
title: '[BUG] <short-description-here>'
|
||||||
title: '[BUG] <description>'
|
labels: [bug]
|
||||||
|
|
||||||
body:
|
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 asked questions](https://manim-slides.eertmans.be/latest/faq.html);
|
||||||
|
required: true
|
||||||
|
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
|
||||||
|
required: true
|
||||||
|
- label: Created a virtual environment in which I can reproduce my bug;
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Describe the issue
|
||||||
description: A brief description of the question or issue, also include what you tried and what didn't work
|
description: A description of the issue, also include what you tried and what didn't work.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
id: version
|
- type: input
|
||||||
|
id: command
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Command
|
||||||
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
|
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:
|
validations:
|
||||||
required: true
|
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
|
- 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
|
id: platform
|
||||||
attributes:
|
attributes:
|
||||||
label: Platform
|
label: What is your platform?
|
||||||
description: What is your platform. Linux, macOS, or Windows?
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Other (please precise below)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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 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
|
- type: textarea
|
||||||
id: screenshots
|
id: screenshots
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots
|
label: Screenshots
|
||||||
description: Please add screenshots if applicable
|
description: Please add screenshots if applicable.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: extrainfo
|
id: extra-info
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional information
|
label: Additional information
|
||||||
description: Is there anything else we should know about this bug?
|
description: Is there anything else we should know about this bug?
|
||||||
validations:
|
validations:
|
||||||
required: false
|
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
|
name: Documentation
|
||||||
description: Ask / Report an issue related to the documentation.
|
description: Ask / Report an issue related to the documentation.
|
||||||
title: 'DOC: <description>'
|
title: '[DOC] <short-description-here>'
|
||||||
labels: [bug, docs]
|
labels: [documentation]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
id: preamble
|
||||||
attributes:
|
attributes:
|
||||||
value: >
|
value: |
|
||||||
**Thank you for wanting to report a problem with manim-slides docs!**
|
**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!
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
⚠
|
label: Terms
|
||||||
Verify first that your issue is not already reported on GitHub [Issues].
|
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;
|
||||||
[Issues]:
|
required: true
|
||||||
https://github.com/jeertmans/manim-slides/issues
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the Issue
|
label: Describe the issue
|
||||||
description: A clear and concise description of the issue you encountered.
|
description: A clear and concise description of the issue you encountered.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: textarea
|
||||||
|
id: pages
|
||||||
attributes:
|
attributes:
|
||||||
label: Affected Page
|
label: Affected page(s)
|
||||||
description: Add a link to page with the problem.
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
id: issue-type
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue Type
|
label: Issue type
|
||||||
description: >
|
description: >
|
||||||
Please select the option in the drop-down.
|
Please select the option in the drop-down.
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<em>Issue?</em>
|
|
||||||
</summary>
|
|
||||||
</details>
|
|
||||||
options:
|
options:
|
||||||
- Documentation Enhancement
|
- Typo, spelling mistake, broken link, etc.
|
||||||
- Documentation Report
|
- Something is missing
|
||||||
|
- Documentation enhancement
|
||||||
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
id: suggested-fix
|
||||||
attributes:
|
attributes:
|
||||||
label: Recommended fix or suggestions
|
label: Recommended fix or suggestions
|
||||||
description: A clear and concise description of how you want to update it.
|
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!
|
description: Have a new idea/feature? Please suggest!
|
||||||
labels: enhancement
|
title: '[FEATURE] <short-description-here>'
|
||||||
title: '[FEATURE] <description>'
|
labels: [enhancement]
|
||||||
|
|
||||||
body:
|
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%3Aenhancement+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
@ -10,6 +28,7 @@ body:
|
|||||||
description: A brief description of the enhancement you propose, also include what you tried and what worked.
|
description: A brief description of the enhancement you propose, also include what you tried and what worked.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshots
|
id: screenshots
|
||||||
attributes:
|
attributes:
|
||||||
@ -17,6 +36,7 @@ body:
|
|||||||
description: Please add screenshots if applicable
|
description: Please add screenshots if applicable
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: extrainfo
|
id: extrainfo
|
||||||
attributes:
|
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 -->
|
<!-- 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.
|
Check all the applicable boxes:
|
||||||
- [ ] If I created new functions / methods, I documented them and add type hints.
|
|
||||||
- [ ] If I modified already existing code, I updated the documentation accordingly.
|
- [ ] 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.
|
- [ ] The title of my pull request is a short description of the requested changes.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
20
.github/workflows/publish.yml
vendored
20
.github/workflows/publish.yml
vendored
@ -18,18 +18,13 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Rye
|
- name: Setup uv
|
||||||
env:
|
uses: astral-sh/setup-uv@v4
|
||||||
RYE_INSTALL_OPTION: --yes
|
with:
|
||||||
run: |
|
enable-cache: true
|
||||||
curl -sSf https://rye-up.com/get | bash
|
|
||||||
echo "$HOME/.rye/shims" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Configure Rye
|
|
||||||
run: rye config --set-bool behavior.use-uv=true
|
|
||||||
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: rye build
|
run: uv build
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||||
@ -41,7 +36,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -68,11 +62,11 @@ jobs:
|
|||||||
print(f"tag_name={ref_tag}", file=f)
|
print(f"tag_name={ref_tag}", file=f)
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64,linux/amd64
|
platforms: linux/arm64,linux/amd64
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
push: true
|
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/jeertmans/manim-slides:latest
|
ghcr.io/jeertmans/manim-slides:latest
|
||||||
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
|
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
|
||||||
|
95
.github/workflows/tests.yml
vendored
95
.github/workflows/tests.yml
vendored
@ -7,7 +7,54 @@ on:
|
|||||||
|
|
||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
pip-install:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [macos-13, 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 install software-properties-common
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt-get install build-essential python${{ matrix.pyversion }}-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||||
|
nohup Xvfb $DISPLAY &
|
||||||
|
|
||||||
|
- 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:
|
pytest:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -18,47 +65,26 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
QT_QPA_PLATFORM: offscreen
|
QT_QPA_PLATFORM: offscreen
|
||||||
MANIM_SLIDES_VERBOSITY: error
|
MANIM_SLIDES_VERBOSITY: error
|
||||||
PYTHONFAULTHANDLER: 1
|
|
||||||
DISPLAY: :99
|
DISPLAY: :99
|
||||||
GITHUB_WORKFLOWS: 1
|
GITHUB_WORKFLOWS: 1
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Rye
|
- name: Setup uv
|
||||||
if: matrix.os != 'windows-latest'
|
uses: astral-sh/setup-uv@v4
|
||||||
env:
|
with:
|
||||||
RYE_TOOLCHAIN_VERSION: ${{ matrix.pyversion}}
|
enable-cache: true
|
||||||
RYE_INSTALL_OPTION: --yes
|
|
||||||
run: |
|
|
||||||
curl -sSf https://rye-up.com/get | bash
|
|
||||||
echo "$HOME/.rye/shims" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
# Stolen from https://github.com/bluss/pyproject-local-kernel/blob/2b641290694adc998fb6bceea58d3737523a68b7/.github/workflows/ci.yaml
|
|
||||||
- name: Install Rye (Windows)
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
C:/msys64/usr/bin/wget.exe -q 'https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe' -O rye-x86_64-windows.exe
|
|
||||||
./rye-x86_64-windows.exe self install --toolchain-version ${{ matrix.pyversion }} --modify-path -y
|
|
||||||
echo "$HOME\\.rye\\shims" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Configure Rye
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rye config --set-bool behavior.use-uv=true
|
|
||||||
rye pin ${{ matrix.pyversion }}
|
|
||||||
|
|
||||||
- name: Install manim dependencies on MacOS
|
- name: Install manim dependencies on MacOS
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: brew install ffmpeg py3cairo
|
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
||||||
|
|
||||||
- name: Install manim dependencies on Ubuntu
|
- name: Install manim dependencies on Ubuntu
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
|
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||||
sudo apt-get install xvfb
|
|
||||||
nohup Xvfb $DISPLAY &
|
nohup Xvfb $DISPLAY &
|
||||||
|
|
||||||
- name: Install Windows dependencies
|
- name: Install Windows dependencies
|
||||||
@ -69,22 +95,11 @@ jobs:
|
|||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: ssciwr/setup-mesa-dist-win@v2
|
uses: ssciwr/setup-mesa-dist-win@v2
|
||||||
|
|
||||||
- name: Install Manim Slides
|
|
||||||
shell: bash
|
|
||||||
run: rye sync
|
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
shell: bash
|
run: uv run --python ${{ matrix.pyversion }} --frozen --group tests --no-dev pytest
|
||||||
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
|
|
||||||
run: rye run pytest
|
|
||||||
|
|
||||||
- name: Run pytest and coverage
|
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
|
|
||||||
run: rye run pytest --cov-report xml --cov=manim_slides tests/
|
|
||||||
|
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
|
uses: codecov/codecov-action@v5
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,7 +4,6 @@ __pycache__/
|
|||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.pdm-python
|
|
||||||
|
|
||||||
# Manim files
|
# Manim files
|
||||||
images/
|
images/
|
||||||
@ -45,6 +44,7 @@ paper/paper.pdf
|
|||||||
paper/media/
|
paper/media/
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
|
.coverage*
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|
||||||
rendering_times.csv
|
rendering_times.csv
|
||||||
|
@ -1,36 +1,38 @@
|
|||||||
|
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:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||||
rev: v2.13.0
|
rev: v2.14.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pretty-format-yaml
|
- id: pretty-format-yaml
|
||||||
args: [--autofix]
|
args: [--autofix]
|
||||||
- id: pretty-format-toml
|
- id: pretty-format-toml
|
||||||
exclude: poetry.lock
|
exclude: poetry.lock
|
||||||
args: [--autofix, --trailing-commas]
|
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
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.3.7
|
rev: v0.11.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.9.0
|
rev: v1.15.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies: [types-requests, types-setuptools]
|
additional_dependencies: [types-requests, types-setuptools]
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
@ -1 +1 @@
|
|||||||
3.11.8
|
3.11
|
||||||
|
@ -2,22 +2,17 @@ version: 2
|
|||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
tools:
|
tools:
|
||||||
python: '3.10'
|
python: '3.11'
|
||||||
apt_packages:
|
apt_packages:
|
||||||
- libpango1.0-dev
|
- libpango1.0-dev
|
||||||
- ffmpeg
|
- ffmpeg
|
||||||
jobs:
|
jobs:
|
||||||
post_install:
|
post_create_environment:
|
||||||
- ipython kernel install --name "manim-slides" --user
|
- asdf plugin add uv
|
||||||
|
- asdf install uv latest
|
||||||
|
- asdf global uv latest
|
||||||
|
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs --no-dev --no-cache
|
||||||
sphinx:
|
sphinx:
|
||||||
builder: html
|
builder: html
|
||||||
configuration: docs/source/conf.py
|
configuration: docs/source/conf.py
|
||||||
fail_on_warning: true
|
fail_on_warning: true
|
||||||
python:
|
|
||||||
install:
|
|
||||||
- method: pip
|
|
||||||
path: .
|
|
||||||
extra_requirements:
|
|
||||||
- docs
|
|
||||||
- magic
|
|
||||||
- sphinx-directive
|
|
||||||
|
282
CHANGELOG.md
282
CHANGELOG.md
@ -8,13 +8,288 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
<!-- start changelog -->
|
<!-- start changelog -->
|
||||||
|
|
||||||
(unreleased)=
|
(unreleased)=
|
||||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.5...HEAD)
|
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.1...HEAD)
|
||||||
|
|
||||||
|
(unreleased-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `manim-slides render` now exits with the same return code as the one returned by `manim render` or `manimgl`.
|
||||||
|
[@chrjabs](https://github.com/chrjabs) [#545](https://github.com/jeertmans/manim-slides/pull/545)
|
||||||
|
|
||||||
|
(unreleased-chore)=
|
||||||
|
### Chore
|
||||||
|
|
||||||
|
- Moved `docs` and `tests` extras, as well as `dev-dependencies`,
|
||||||
|
inside groups in `dependency-groups`. This could break existing code
|
||||||
|
when using one of those extras, but as they were not part of the public API,
|
||||||
|
we do not consider this to be a **breaking change**.
|
||||||
|
[#542](https://github.com/jeertmans/manim-slides/pull/542)
|
||||||
|
|
||||||
|
(v5.5.1)=
|
||||||
|
## [v5.5.1](https://github.com/jeertmans/manim-slides/compare/v5.5.0...v5.5.1)
|
||||||
|
|
||||||
|
(v5.5.1-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- HTML template now always includes the *notes* plugin so that the speaker
|
||||||
|
view is always available. Previously, it was only included if the slides
|
||||||
|
had notes.
|
||||||
|
[#538](https://github.com/jeertmans/manim-slides/pull/538)
|
||||||
|
- Pressing <kbd>SPACE</kbd> key now pauses the slides, instead of skipping it.
|
||||||
|
Previously, it was not possible to pause HTML slides, which can be very annoying
|
||||||
|
when trying to explain something.
|
||||||
|
[#539](https://github.com/jeertmans/manim-slides/pull/539)
|
||||||
|
|
||||||
|
(v5.5.0)=
|
||||||
|
## [v5.5.0](https://github.com/jeertmans/manim-slides/compare/v5.4.2...v5.5.0)
|
||||||
|
|
||||||
|
(v5.5.0-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `max_duration_before_split_reverse` and `num_processes` class variables.
|
||||||
|
[#439](https://github.com/jeertmans/manim-slides/pull/439)
|
||||||
|
- Added `src = ...` filepath argument to allow inserting external
|
||||||
|
videos as slides.
|
||||||
|
[#526](https://github.com/jeertmans/manim-slides/pull/526)
|
||||||
|
|
||||||
|
(v5.5.0-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Automatically split large video animations into smaller chunks
|
||||||
|
for lightweight (and potentially faster) reversed animations generation.
|
||||||
|
[#439](https://github.com/jeertmans/manim-slides/pull/439)
|
||||||
|
|
||||||
|
(v5.5.0-chore)=
|
||||||
|
### Chore
|
||||||
|
|
||||||
|
- Trimmed whitespaces in HTML template.
|
||||||
|
[#443](https://github.com/jeertmans/manim-slides/pull/443)
|
||||||
|
- Bumped RevealJS' version to 5.2 to allow video playing in speaker view.
|
||||||
|
[#536](https://github.com/jeertmans/manim-slides/pull/536)
|
||||||
|
|
||||||
|
(v5.4.2)=
|
||||||
|
## [v5.4.2](https://github.com/jeertmans/manim-slides/compare/v5.4.1...v5.4.2)
|
||||||
|
|
||||||
|
(v5.4.2-fixed)=
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `start_skip_animations` to actually pass argument to ManimCE,
|
||||||
|
otherwise video animations were still rendered, just excluded from
|
||||||
|
the final output.
|
||||||
|
[#524](https://github.com/jeertmans/manim-slides/pull/524)
|
||||||
|
|
||||||
|
(v5.4.1)=
|
||||||
|
## [v5.4.1](https://github.com/jeertmans/manim-slides/compare/v5.4.0...v5.4.1)
|
||||||
|
|
||||||
|
(v5.4.1-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `start_skip_animations` and `stop_skip_animations` methods.
|
||||||
|
[#523](https://github.com/jeertmans/manim-slides/pull/523)
|
||||||
|
|
||||||
|
(v5.4.0)=
|
||||||
|
## [v5.4.0](https://github.com/jeertmans/manim-slides/compare/v5.3.1...v5.4.0)
|
||||||
|
|
||||||
|
(v5.4.0-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `skip_animations` compatibility with ManimCE.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#516](https://github.com/jeertmans/manim-slides/pull/516)
|
||||||
|
|
||||||
|
(v5.4.0-chore)=
|
||||||
|
### Chore
|
||||||
|
|
||||||
|
- Bumped Manim to `>=0.19`, as it fixed OpenGL renderer issue.
|
||||||
|
[#522](https://github.com/jeertmans/manim-slides/pull/522)
|
||||||
|
|
||||||
|
(v5.4.0-fixed)=
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed OpenGL renderer having no partial movie files with Manim bindings.
|
||||||
|
[#522](https://github.com/jeertmans/manim-slides/pull/522)
|
||||||
|
- Fixed `ConvertExample` example as `manim>=0.19` changed the `Code` class.
|
||||||
|
[#522](https://github.com/jeertmans/manim-slides/pull/522)
|
||||||
|
|
||||||
|
(v5.3.1)=
|
||||||
|
## [v5.3.1](https://github.com/jeertmans/manim-slides/compare/v5.3.0...v5.3.1)
|
||||||
|
|
||||||
|
(v5.3.1-fixed)=
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed HTML template to avoid missing slides when exporting with `--one-file`.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#515](https://github.com/jeertmans/manim-slides/pull/515)
|
||||||
|
|
||||||
|
(v5.3.0)=
|
||||||
|
## [v5.3.0](https://github.com/jeertmans/manim-slides/compare/v5.2.0...v5.3.0)
|
||||||
|
|
||||||
|
(v5.3.0-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added CSS and JS inline for `manim-slides convert` if `--offline`
|
||||||
|
and `--one-file` (`-cone_file`) are used for HTML output.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
|
||||||
|
|
||||||
|
(v5.3.0-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Deprecate `-cdata_uri` in favor of `-cone_file` for `manim-slides convert`.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
|
||||||
|
- Changed template to avoid micro-stuttering with `--one-file` in HTML presentation.
|
||||||
|
[@Rapsssito](https://github.com/Rapsssito) [#508](https://github.com/jeertmans/manim-slides/pull/508)
|
||||||
|
|
||||||
|
(v5.2.0)=
|
||||||
|
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
|
||||||
|
|
||||||
|
(v5.2.0-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The info window is now only shown in presentations when there
|
||||||
|
are multiple monitors. However, the `--show-info-window` option
|
||||||
|
was added to `manim-slides present` to force the info window.
|
||||||
|
When there are multiple monitors, the info window will no longer
|
||||||
|
be on the same monitor as the main window, unless overridden.
|
||||||
|
[@taibeled](https://github.com/taibeled)
|
||||||
|
[#482](https://github.com/jeertmans/manim-slides/pull/482)
|
||||||
|
|
||||||
|
(v5.2.0-chore)=
|
||||||
|
### Chore
|
||||||
|
|
||||||
|
- Bumped ManimGL to `>=1.7.1`, to remove conflicting dependencies
|
||||||
|
with Manim's.
|
||||||
|
[#499](https://github.com/jeertmans/manim-slides/pull/499)
|
||||||
|
|
||||||
|
- Bumped ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies,
|
||||||
|
and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`,
|
||||||
|
as the latter includes many breaking changes.
|
||||||
|
[#506](https://github.com/jeertmans/manim-slides/pull/506)
|
||||||
|
|
||||||
|
(v5.1.10)=
|
||||||
|
## [v5.1.10](https://github.com/jeertmans/manim-slides/compare/v5.1.9...v5.1.10)
|
||||||
|
|
||||||
|
(v5.1.10-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `--offline` option to `manim-slides convert` for offline
|
||||||
|
HTML presentations.
|
||||||
|
[#440](https://github.com/jeertmans/manim-slides/pull/440)
|
||||||
|
- Added documentation to config option to `manim-slides convert`
|
||||||
|
when using `--show-config`.
|
||||||
|
[#485](https://github.com/jeertmans/manim-slides/pull/485)
|
||||||
|
|
||||||
|
(v5.1.10-changed)=
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Allow multiple slide reverses by going backward [@taibeled](https://github.com/taibeled).
|
||||||
|
[#488](https://github.com/jeertmans/manim-slides/pull/488)
|
||||||
|
|
||||||
|
(v5.1.10-fixed)=
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed PyAV issue by pinning its version to `<14`.
|
||||||
|
A future release will contain a fix that supports both `av>=14`
|
||||||
|
and `av<14`, as their syntax differ, but the former doesn't
|
||||||
|
provide binary wheels for Python 3.9.
|
||||||
|
[#494](https://github.com/jeertmans/manim-slides/pull/494)
|
||||||
|
- Fixed blank web page when converting multiple slides into HTML.
|
||||||
|
[#497](https://github.com/jeertmans/manim-slides/pull/497)
|
||||||
|
|
||||||
|
(v5.1.9)=
|
||||||
|
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)
|
||||||
|
|
||||||
|
(v5.1.9-fixed)=
|
||||||
|
## Chore
|
||||||
|
|
||||||
|
- Fixed failing docker builds.
|
||||||
|
[#481](https://github.com/jeertmans/manim-slides/pull/481)
|
||||||
|
|
||||||
|
(v5.1.8)=
|
||||||
|
## [v5.1.8](https://github.com/jeertmans/manim-slides/compare/v5.1.7...v5.1.8)
|
||||||
|
|
||||||
|
(v5.1.8-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `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
|
||||||
|
|
||||||
|
- Pinned `rtoml==0.9.0` on Windows platforms,
|
||||||
|
see [#398](https://github.com/jeertmans/manim-slides/pull/398),
|
||||||
|
until
|
||||||
|
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
|
||||||
|
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 [@taibeled](https://github.com/taibeled)!
|
||||||
|
[#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)=
|
||||||
## [v5.1.5](https://github.com/jeertmans/manim-slides/compare/v5.1.4...v5.1.5)
|
## [v5.1.5](https://github.com/jeertmans/manim-slides/compare/v5.1.4...v5.1.5)
|
||||||
|
|
||||||
(v5.1.5)=
|
(v5.1.5-chore)=
|
||||||
### Chore
|
### Chore
|
||||||
|
|
||||||
- Added CI for broken HTML links and fixed, plus spell checking.
|
- Added CI for broken HTML links and fixed, plus spell checking.
|
||||||
@ -24,13 +299,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Used Rye instead of PDM for faster development.
|
- Used Rye instead of PDM for faster development.
|
||||||
[#420](https://github.com/jeertmans/manim-slides/pull/420)
|
[#420](https://github.com/jeertmans/manim-slides/pull/420)
|
||||||
|
|
||||||
(v5.1.5)=
|
(v5.1.5-fixed)=
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed broken `--show-config` command.
|
- Fixed broken `--show-config` command.
|
||||||
[#419](https://github.com/jeertmans/manim-slides/pull/419)
|
[#419](https://github.com/jeertmans/manim-slides/pull/419)
|
||||||
|
|
||||||
|
|
||||||
(v5.1.4)=
|
(v5.1.4)=
|
||||||
## [v5.1.4](https://github.com/jeertmans/manim-slides/compare/v5.1.3...v5.1.4)
|
## [v5.1.4](https://github.com/jeertmans/manim-slides/compare/v5.1.3...v5.1.4)
|
||||||
|
|
||||||
|
@ -4,13 +4,14 @@
|
|||||||
cff-version: 1.2.0
|
cff-version: 1.2.0
|
||||||
title: Manim Slides
|
title: Manim Slides
|
||||||
message: >-
|
message: >-
|
||||||
If you use this software, please cite it using the
|
If you use this software, please cite it using our article
|
||||||
metadata from this file.
|
in the Journal of Open Source Education.
|
||||||
type: software
|
type: software
|
||||||
authors:
|
authors:
|
||||||
- name: Jérome Eertmans
|
- name: Jérome Eertmans
|
||||||
orcid: 'https://orcid.org/0000-0002-5579-5360'
|
orcid: 'https://orcid.org/0000-0002-5579-5360'
|
||||||
website: 'https://eertmans.be'
|
website: 'https://eertmans.be'
|
||||||
|
doi: 10.5281/zenodo.7971360
|
||||||
repository-code: 'https://github.com/jeertmans/manim-slides'
|
repository-code: 'https://github.com/jeertmans/manim-slides'
|
||||||
url: 'https://eertmans.be/manim-slides'
|
url: 'https://eertmans.be/manim-slides'
|
||||||
abstract: >-
|
abstract: >-
|
||||||
@ -26,7 +27,7 @@ keywords:
|
|||||||
- PowerPoint
|
- PowerPoint
|
||||||
- Python
|
- Python
|
||||||
license: MIT
|
license: MIT
|
||||||
version: v5.1.5
|
version: v5.5.1
|
||||||
preferred-citation:
|
preferred-citation:
|
||||||
publisher:
|
publisher:
|
||||||
name: The Open Journal
|
name: The Open Journal
|
||||||
|
25
README.md
25
README.md
@ -4,6 +4,8 @@
|
|||||||
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
|
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
|
<!-- start pypi -->
|
||||||
|
|
||||||
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
||||||
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
||||||
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
|
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
|
||||||
@ -29,6 +31,7 @@ Manim Slides will *automatically* detect the one you are using!
|
|||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Comparison with Similar Tools](#comparison-with-similar-tools)
|
- [Comparison with Similar Tools](#comparison-with-similar-tools)
|
||||||
- [F.A.Q](https://eertmans.be/manim-slides/latest/faq.html)
|
- [F.A.Q](https://eertmans.be/manim-slides/latest/faq.html)
|
||||||
|
- [Citing](#citing)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
* [Reporting an Issue](#reporting-an-issue)
|
* [Reporting an Issue](#reporting-an-issue)
|
||||||
* [Seeking for Help](#seeking-for-help)
|
* [Seeking for Help](#seeking-for-help)
|
||||||
@ -149,6 +152,24 @@ Below is a comparison of the most used ones with Manim Slides:
|
|||||||
| Web Browser presentations | Yes | No | Yes | No |
|
| Web Browser presentations | Yes | No | Yes | No |
|
||||||
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
|
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
|
||||||
|
|
||||||
|
## Citing
|
||||||
|
|
||||||
|
If you use this project, please cite it using the following reference:
|
||||||
|
|
||||||
|
```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}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or by linking this GitHub repository at the end of the presentation.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are more than welcome! Please read through
|
Contributions are more than welcome! Please read through
|
||||||
@ -207,8 +228,8 @@ you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
|
|||||||
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
|
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
|
||||||
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
|
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
|
||||||
[documentation-url]: https://manim-slides.readthedocs.io/
|
[documentation-url]: https://manim-slides.readthedocs.io/
|
||||||
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
|
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7971360.svg
|
||||||
[doi-url]: https://doi.org/10.5281/zenodo.8215167
|
[doi-url]: https://doi.org/10.5281/zenodo.7971360
|
||||||
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
|
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
|
||||||
[jose-url]: https://doi.org/10.21105/jose.00206
|
[jose-url]: https://doi.org/10.21105/jose.00206
|
||||||
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
|
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
style:
|
camera:
|
||||||
background_color: '#000000'
|
background_color: '#000000'
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
|
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
RUN apt-get update -qq \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install --no-install-recommends -y \
|
&& apt-get install --no-install-recommends -y \
|
||||||
ffmpeg \
|
|
||||||
build-essential \
|
build-essential \
|
||||||
gcc \
|
gcc \
|
||||||
cmake \
|
cmake \
|
||||||
@ -24,21 +23,22 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
|
|||||||
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
|
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
|
||||||
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
||||||
&& tlmgr install \
|
&& tlmgr install \
|
||||||
amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \
|
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||||
mathastext microtype ms physics preview ragged2e relsize rsfs \
|
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||||
|
|
||||||
# clone and build manim-slides
|
# clone and build manim-slides
|
||||||
COPY . /opt/manim-slides
|
COPY . /opt/manim-slides
|
||||||
WORKDIR /opt/manim-slides
|
WORKDIR /opt/manim-slides
|
||||||
RUN pip install --no-cache manim[jupyterlab] .[sphinx-directive]
|
|
||||||
|
RUN pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
|
||||||
|
|
||||||
ARG NB_USER=manimslidesuser
|
ARG NB_USER=manimslidesuser
|
||||||
ARG NB_UID=1000
|
ARG NB_UID=1000
|
||||||
ENV USER ${NB_USER}
|
ENV USER=${NB_USER}
|
||||||
ENV NB_UID ${NB_UID}
|
ENV NB_UID=${NB_UID}
|
||||||
ENV HOME /manim-slides
|
ENV HOME=/manim-slides
|
||||||
|
|
||||||
RUN adduser --disabled-password \
|
RUN adduser --disabled-password \
|
||||||
--gecos "Default user" \
|
--gecos "Default user" \
|
||||||
|
@ -18,82 +18,75 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="reveal">
|
<div class="reveal">
|
||||||
<div class="slides">
|
<div class="slides">
|
||||||
{%- for presentation_config in presentation_configs -%}
|
{% for presentation_config in presentation_configs -%}
|
||||||
{% set outer_loop = loop %}
|
{%- set outer_loop = loop %}
|
||||||
{%- for slide_config in presentation_config.slides -%}
|
{% for slide_config in presentation_config.slides %}
|
||||||
{%- if data_uri -%}
|
{% if one_file %}
|
||||||
{% set file = file_to_data_uri(slide_config.file) %}
|
{% set file = file_to_data_uri(slide_config.file) %}
|
||||||
{%- else -%}
|
{% else %}
|
||||||
{% set file = assets_dir / slide_config.file.name %}
|
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
<section
|
<section
|
||||||
data-background-size={{ background_size }}
|
data-background-size={{ background_size }}
|
||||||
data-background-color="{{ presentation_config.background_color }}"
|
data-background-color="{{ presentation_config.background_color }}"
|
||||||
data-background-video="{{ file }}"
|
data-background-video="{{ file }}"
|
||||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
{% if loop.index == 1 and outer_loop.index == 1 %}
|
||||||
data-background-video-muted
|
data-background-video-muted
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
{% if slide_config.loop -%}
|
{% if slide_config.loop %}
|
||||||
data-background-video-loop
|
data-background-video-loop
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
{% if slide_config.auto_next -%}
|
{% if slide_config.auto_next %}
|
||||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||||
{%- endif -%}>
|
{% endif %}
|
||||||
{% if slide_config.notes != "" -%}
|
>
|
||||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
{% if slide_config.notes != "" %}
|
||||||
{%- endif %}
|
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||||
</section>
|
{% endif %}
|
||||||
{%- endfor -%}
|
</section>
|
||||||
{%- endfor -%}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
|
||||||
|
|
||||||
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
|
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
|
||||||
|
{% if has_notes %}
|
||||||
{% if has_notes -%}
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
|
|
||||||
<!-- <script src="index.js"></script> -->
|
<!-- <script src="index.js"></script> -->
|
||||||
<script>
|
<script>
|
||||||
Reveal.initialize({
|
Reveal.initialize({
|
||||||
{% if has_notes -%}
|
{% if has_notes %}
|
||||||
plugins: [ RevealMarkdown, RevealNotes ],
|
/// The list of RevealJS plugins.
|
||||||
{%- endif %}
|
plugins: [ RevealMarkdown, RevealNotes ],
|
||||||
|
{% endif %}
|
||||||
// The "normal" size of the presentation, aspect ratio will
|
// The "normal" size of the presentation, aspect ratio will
|
||||||
// be preserved when the presentation is scaled to fit different
|
// be preserved when the presentation is scaled to fit different
|
||||||
// resolutions. Can be specified using percentage units.
|
// resolutions. Can be specified using percentage units.
|
||||||
width: {{ width }},
|
width: {{ width }},
|
||||||
height: {{ height }},
|
height: {{ height }},
|
||||||
|
|
||||||
// Factor of the display size that should remain empty around
|
// Factor of the display size that should remain empty around
|
||||||
// the content
|
// the content
|
||||||
margin: {{ margin }},
|
margin: {{ margin }},
|
||||||
|
|
||||||
// Bounds for smallest/largest possible scale to apply to content
|
// Bounds for smallest/largest possible scale to apply to content
|
||||||
minScale: {{ min_scale }},
|
minScale: {{ min_scale }},
|
||||||
maxScale: {{ max_scale }},
|
maxScale: {{ max_scale }},
|
||||||
|
|
||||||
// Display presentation control arrows
|
// Display presentation control arrows
|
||||||
controls: {{ controls }},
|
controls: {{ controls }},
|
||||||
|
|
||||||
// Help the user learn the controls by providing hints, for example by
|
// Help the user learn the controls by providing hints, for example by
|
||||||
// bouncing the down arrow when they first encounter a vertical slide
|
// bouncing the down arrow when they first encounter a vertical slide
|
||||||
controlsTutorial: {{ controls_tutorial }},
|
controlsTutorial: {{ controls_tutorial }},
|
||||||
|
|
||||||
// Determines where controls appear, "edges" or "bottom-right"
|
// Determines where controls appear, "edges" or "bottom-right"
|
||||||
controlsLayout: {{ controls_layout }},
|
controlsLayout: {{ controls_layout }},
|
||||||
|
|
||||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
||||||
// or "visible"
|
// or "visible"
|
||||||
controlsBackArrows: {{ controls_back_arrows }},
|
controlsBackArrows: {{ controls_back_arrows }},
|
||||||
|
|
||||||
// Display a presentation progress bar
|
// Display a presentation progress bar
|
||||||
progress: {{ progress }},
|
progress: {{ progress }},
|
||||||
|
|
||||||
// Display the page number of the current slide
|
// Display the page number of the current slide
|
||||||
// - true: Show slide number
|
// - true: Show slide number
|
||||||
// - false: Hide slide number
|
// - false: Hide slide number
|
||||||
@ -109,55 +102,43 @@
|
|||||||
// object and return an array with one string [slideNumber] or
|
// object and return an array with one string [slideNumber] or
|
||||||
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
||||||
slideNumber: {{ slide_number }},
|
slideNumber: {{ slide_number }},
|
||||||
|
|
||||||
// Can be used to limit the contexts in which the slide number appears
|
// Can be used to limit the contexts in which the slide number appears
|
||||||
// - "all": Always show the slide number
|
// - "all": Always show the slide number
|
||||||
// - "print": Only when printing to PDF
|
// - "print": Only when printing to PDF
|
||||||
// - "speaker": Only in the speaker view
|
// - "speaker": Only in the speaker view
|
||||||
showSlideNumber: {{ show_slide_number }},
|
showSlideNumber: {{ show_slide_number }},
|
||||||
|
|
||||||
// Use 1 based indexing for # links to match slide number (default is zero
|
// Use 1 based indexing for # links to match slide number (default is zero
|
||||||
// based)
|
// based)
|
||||||
hashOneBasedIndex: {{ hash_one_based_index }},
|
hashOneBasedIndex: {{ hash_one_based_index }},
|
||||||
|
|
||||||
// Add the current slide number to the URL hash so that reloading the
|
// Add the current slide number to the URL hash so that reloading the
|
||||||
// page/copying the URL will return you to the same slide
|
// page/copying the URL will return you to the same slide
|
||||||
hash: {{ hash }},
|
hash: {{ hash }},
|
||||||
|
|
||||||
// Flags if we should monitor the hash and change slides accordingly
|
// Flags if we should monitor the hash and change slides accordingly
|
||||||
respondToHashChanges: {{ respond_to_hash_changes }},
|
respondToHashChanges: {{ respond_to_hash_changes }},
|
||||||
|
// Enable support for jump-to-slide navigation shortcuts
|
||||||
|
jumpToSlide: {{ jump_to_slide }},
|
||||||
// Push each slide change to the browser history. Implies `hash: true`
|
// Push each slide change to the browser history. Implies `hash: true`
|
||||||
history: {{ history }},
|
history: {{ history }},
|
||||||
|
|
||||||
// Enable keyboard shortcuts for navigation
|
// Enable keyboard shortcuts for navigation
|
||||||
keyboard: {{ keyboard }},
|
keyboard: {{ keyboard }},
|
||||||
|
|
||||||
// Optional function that blocks keyboard events when retuning false
|
// Optional function that blocks keyboard events when retuning false
|
||||||
//
|
//
|
||||||
// If you set this to 'focused', we will only capture keyboard events
|
// If you set this to 'focused', we will only capture keyboard events
|
||||||
// for embedded decks when they are in focus
|
// for embedded decks when they are in focus
|
||||||
keyboardCondition: {{ keyboard_condition }},
|
keyboardCondition: {{ keyboard_condition }},
|
||||||
|
|
||||||
// Disables the default reveal.js slide layout (scaling and centering)
|
// Disables the default reveal.js slide layout (scaling and centering)
|
||||||
// so that you can use custom CSS layout
|
// so that you can use custom CSS layout
|
||||||
disableLayout: {{ disable_layout }},
|
disableLayout: {{ disable_layout }},
|
||||||
|
|
||||||
// Enable the slide overview mode
|
// Enable the slide overview mode
|
||||||
overview: {{ overview }},
|
overview: {{ overview }},
|
||||||
|
|
||||||
// Vertical centering of slides
|
// Vertical centering of slides
|
||||||
center: {{ center }},
|
center: {{ center }},
|
||||||
|
|
||||||
// Enables touch navigation on devices with touch input
|
// Enables touch navigation on devices with touch input
|
||||||
touch: {{ touch }},
|
touch: {{ touch }},
|
||||||
|
|
||||||
// Loop the presentation
|
// Loop the presentation
|
||||||
loop: {{ loop }},
|
loop: {{ loop }},
|
||||||
|
|
||||||
// Change the presentation direction to be RTL
|
// Change the presentation direction to be RTL
|
||||||
rtl: {{ rtl }},
|
rtl: {{ rtl }},
|
||||||
|
|
||||||
// Changes the behavior of our navigation directions.
|
// Changes the behavior of our navigation directions.
|
||||||
//
|
//
|
||||||
// "default"
|
// "default"
|
||||||
@ -183,159 +164,146 @@
|
|||||||
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
||||||
// from 1.3 -> 2.3.
|
// from 1.3 -> 2.3.
|
||||||
navigationMode: {{ navigation_mode }},
|
navigationMode: {{ navigation_mode }},
|
||||||
|
|
||||||
// Randomizes the order of slides each time the presentation loads
|
// Randomizes the order of slides each time the presentation loads
|
||||||
shuffle: {{ shuffle }},
|
shuffle: {{ shuffle }},
|
||||||
|
|
||||||
// Turns fragments on and off globally
|
// Turns fragments on and off globally
|
||||||
fragments: {{ fragments }},
|
fragments: {{ fragments }},
|
||||||
|
|
||||||
// Flags whether to include the current fragment in the URL,
|
// Flags whether to include the current fragment in the URL,
|
||||||
// so that reloading brings you to the same fragment position
|
// so that reloading brings you to the same fragment position
|
||||||
fragmentInURL: {{ fragment_in_url }},
|
fragmentInURL: {{ fragment_in_url }},
|
||||||
|
|
||||||
// Flags if the presentation is running in an embedded mode,
|
// Flags if the presentation is running in an embedded mode,
|
||||||
// i.e. contained within a limited portion of the screen
|
// i.e. contained within a limited portion of the screen
|
||||||
embedded: {{ embedded }},
|
embedded: {{ embedded }},
|
||||||
|
|
||||||
// Flags if we should show a help overlay when the question-mark
|
// Flags if we should show a help overlay when the question-mark
|
||||||
// key is pressed
|
// key is pressed
|
||||||
help: {{ help }},
|
help: {{ help }},
|
||||||
|
|
||||||
// Flags if it should be possible to pause the presentation (blackout)
|
// Flags if it should be possible to pause the presentation (blackout)
|
||||||
pause: {{ pause }},
|
pause: {{ pause }},
|
||||||
|
|
||||||
// Flags if speaker notes should be visible to all viewers
|
// Flags if speaker notes should be visible to all viewers
|
||||||
showNotes: {{ show_notes }},
|
showNotes: {{ show_notes }},
|
||||||
|
|
||||||
// Global override for autolaying embedded media (video/audio/iframe)
|
// Global override for autolaying embedded media (video/audio/iframe)
|
||||||
// - null: Media will only autoplay if data-autoplay is present
|
// - null: Media will only autoplay if data-autoplay is present
|
||||||
// - true: All media will autoplay, regardless of individual setting
|
// - true: All media will autoplay, regardless of individual setting
|
||||||
// - false: No media will autoplay, regardless of individual setting
|
// - false: No media will autoplay, regardless of individual setting
|
||||||
autoPlayMedia: {{ auto_play_media }},
|
autoPlayMedia: {{ auto_play_media }},
|
||||||
|
|
||||||
// Global override for preloading lazy-loaded iframes
|
// Global override for preloading lazy-loaded iframes
|
||||||
// - null: Iframes with data-src AND data-preload will be loaded when within
|
// - null: Iframes with data-src AND data-preload will be loaded when within
|
||||||
// the viewDistance, iframes with only data-src will be loaded when visible
|
// the viewDistance, iframes with only data-src will be loaded when visible
|
||||||
// - true: All iframes with data-src will be loaded when within the viewDistance
|
// - true: All iframes with data-src will be loaded when within the viewDistance
|
||||||
// - false: All iframes with data-src will be loaded only when visible
|
// - false: All iframes with data-src will be loaded only when visible
|
||||||
preloadIframes: {{ preload_iframes }},
|
preloadIframes: {{ preload_iframes }},
|
||||||
|
|
||||||
// Can be used to globally disable auto-animation
|
// Can be used to globally disable auto-animation
|
||||||
autoAnimate: {{ auto_animate }},
|
autoAnimate: {{ auto_animate }},
|
||||||
|
|
||||||
// Optionally provide a custom element matcher that will be
|
// Optionally provide a custom element matcher that will be
|
||||||
// used to dictate which elements we can animate between.
|
// used to dictate which elements we can animate between.
|
||||||
autoAnimateMatcher: {{ auto_animate_matcher }},
|
autoAnimateMatcher: {{ auto_animate_matcher }},
|
||||||
|
|
||||||
// Default settings for our auto-animate transitions, can be
|
// Default settings for our auto-animate transitions, can be
|
||||||
// overridden per-slide or per-element via data arguments
|
// overridden per-slide or per-element via data arguments
|
||||||
autoAnimateEasing: {{ auto_animate_easing }},
|
autoAnimateEasing: {{ auto_animate_easing }},
|
||||||
autoAnimateDuration: {{ auto_animate_duration }},
|
autoAnimateDuration: {{ auto_animate_duration }},
|
||||||
autoAnimateUnmatched: {{ auto_animate_unmatched }},
|
autoAnimateUnmatched: {{ auto_animate_unmatched }},
|
||||||
|
|
||||||
// CSS properties that can be auto-animated. Position & scale
|
// CSS properties that can be auto-animated. Position & scale
|
||||||
// is matched separately so there's no need to include styles
|
// is matched separately so there's no need to include styles
|
||||||
// like top/right/bottom/left, width/height or margin.
|
// like top/right/bottom/left, width/height or margin.
|
||||||
autoAnimateStyles: {{ auto_animate_styles }},
|
autoAnimateStyles: {{ auto_animate_styles }},
|
||||||
|
|
||||||
// Controls automatic progression to the next slide
|
// Controls automatic progression to the next slide
|
||||||
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
||||||
// is present on the current slide or fragment
|
// is present on the current slide or fragment
|
||||||
// - 1+: All slides will progress automatically at the given interval
|
// - 1+: All slides will progress automatically at the given interval
|
||||||
// - false: No auto-sliding, even if data-autoslide is present
|
// - false: No auto-sliding, even if data-autoslide is present
|
||||||
autoSlide: {{ auto_slide }},
|
autoSlide: {{ auto_slide }},
|
||||||
|
|
||||||
// Stop auto-sliding after user input
|
// Stop auto-sliding after user input
|
||||||
autoSlideStoppable: {{ auto_slide_stoppable }},
|
autoSlideStoppable: {{ auto_slide_stoppable }},
|
||||||
|
|
||||||
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
||||||
autoSlideMethod: {{ auto_slide_method }},
|
autoSlideMethod: {{ auto_slide_method }},
|
||||||
|
|
||||||
// Specify the average time in seconds that you think you will spend
|
// Specify the average time in seconds that you think you will spend
|
||||||
// presenting each slide. This is used to show a pacing timer in the
|
// presenting each slide. This is used to show a pacing timer in the
|
||||||
// speaker view
|
// speaker view
|
||||||
defaultTiming: {{ default_timing }},
|
defaultTiming: {{ default_timing }},
|
||||||
|
|
||||||
// Enable slide navigation via mouse wheel
|
// Enable slide navigation via mouse wheel
|
||||||
mouseWheel: {{ mouse_wheel }},
|
mouseWheel: {{ mouse_wheel }},
|
||||||
|
|
||||||
// Opens links in an iframe preview overlay
|
// Opens links in an iframe preview overlay
|
||||||
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
||||||
// individually
|
// individually
|
||||||
previewLinks: {{ preview_links }},
|
previewLinks: {{ preview_links }},
|
||||||
|
|
||||||
// Exposes the reveal.js API through window.postMessage
|
// Exposes the reveal.js API through window.postMessage
|
||||||
postMessage: {{ post_message }},
|
postMessage: {{ post_message }},
|
||||||
|
|
||||||
// Dispatches all reveal.js events to the parent window through postMessage
|
// Dispatches all reveal.js events to the parent window through postMessage
|
||||||
postMessageEvents: {{ post_message_events }},
|
postMessageEvents: {{ post_message_events }},
|
||||||
|
|
||||||
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
||||||
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
|
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
|
||||||
|
|
||||||
// Transition style
|
// Transition style
|
||||||
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
|
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
|
||||||
|
|
||||||
// Transition speed
|
// Transition speed
|
||||||
transitionSpeed: {{ transition_speed }}, // default/fast/slow
|
transitionSpeed: {{ transition_speed }}, // default/fast/slow
|
||||||
|
|
||||||
// Transition style for full page slide backgrounds
|
// Transition style for full page slide backgrounds
|
||||||
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
|
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
|
||||||
|
|
||||||
// The maximum number of pages a single slide can expand onto when printing
|
// The maximum number of pages a single slide can expand onto when printing
|
||||||
// to PDF, unlimited by default
|
// to PDF, unlimited by default
|
||||||
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
|
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
|
||||||
|
|
||||||
// Prints each fragment on a separate slide
|
// Prints each fragment on a separate slide
|
||||||
pdfSeparateFragments: {{ pdf_separate_fragments }},
|
pdfSeparateFragments: {{ pdf_separate_fragments }},
|
||||||
|
|
||||||
// Offset used to reduce the height of content within exported PDF pages.
|
// Offset used to reduce the height of content within exported PDF pages.
|
||||||
// This exists to account for environment differences based on how you
|
// This exists to account for environment differences based on how you
|
||||||
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
||||||
// on precisely the total height of the document whereas in-browser
|
// on precisely the total height of the document whereas in-browser
|
||||||
// printing has to end one pixel before.
|
// printing has to end one pixel before.
|
||||||
pdfPageHeightOffset: {{ pdf_page_height_offset }},
|
pdfPageHeightOffset: {{ pdf_page_height_offset }},
|
||||||
|
|
||||||
// Number of slides away from the current that are visible
|
// Number of slides away from the current that are visible
|
||||||
viewDistance: {{ view_distance }},
|
viewDistance: {{ view_distance }},
|
||||||
|
|
||||||
// Number of slides away from the current that are visible on mobile
|
// Number of slides away from the current that are visible on mobile
|
||||||
// devices. It is advisable to set this to a lower number than
|
// devices. It is advisable to set this to a lower number than
|
||||||
// viewDistance in order to save resources.
|
// viewDistance in order to save resources.
|
||||||
mobileViewDistance: {{ mobile_view_distance }},
|
mobileViewDistance: {{ mobile_view_distance }},
|
||||||
|
|
||||||
// The display mode that will be used to show slides
|
// The display mode that will be used to show slides
|
||||||
display: {{ display }},
|
display: {{ display }},
|
||||||
|
|
||||||
// Hide cursor if inactive
|
// Hide cursor if inactive
|
||||||
hideInactiveCursor: {{ hide_inactive_cursor }},
|
hideInactiveCursor: {{ hide_inactive_cursor }},
|
||||||
|
|
||||||
// Time before the cursor is hidden (in ms)
|
// Time before the cursor is hidden (in ms)
|
||||||
hideCursorTime: {{ hide_cursor_time }}
|
hideCursorTime: {{ hide_cursor_time }}
|
||||||
});
|
});
|
||||||
|
{% if one_file %}
|
||||||
|
// Fix found by @t-fritsch and @Rapsssito on GitHub
|
||||||
|
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
|
||||||
|
function setVideoBase64(video) {
|
||||||
|
const sources = video.querySelectorAll('source');
|
||||||
|
// Update the source of the video
|
||||||
|
sources.forEach((source, i) => {
|
||||||
|
const src = source.getAttribute('src');
|
||||||
|
if(src.match(/^data:video.*;base64$/)) {
|
||||||
|
const nextSrc = sources[i+1]?.getAttribute('src');
|
||||||
|
video.setAttribute('src', `${src},${nextSrc}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{% if data_uri %}
|
function fixBase64VideoBackground(event) {
|
||||||
// Fix found by @t-fritsch on GitHub
|
// Analyze all slides backgrounds
|
||||||
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
|
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
|
||||||
function fixBase64VideoBackground(event) {
|
// Get the slide video and its sources for each background
|
||||||
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
|
const video = slide.querySelector('video');
|
||||||
if (event.currentSlide.getAttribute('data-background-video')) {
|
if (video) {
|
||||||
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
|
setVideoBase64(video);
|
||||||
video = background.querySelector('video'),
|
} else {
|
||||||
sources = video.querySelectorAll('source');
|
// Listen to the creation of the video element
|
||||||
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
sources.forEach((source, i) => {
|
for (const mutation of mutationsList) {
|
||||||
const src = source.getAttribute('src');
|
if (mutation.type === 'childList') {
|
||||||
if(src.match(/^data:video.*;base64$/)) {
|
for (const addedNode of mutation.addedNodes) {
|
||||||
const nextSrc = sources[i+1]?.getAttribute('src');
|
if (addedNode.tagName === 'VIDEO') {
|
||||||
video.setAttribute('src', `${src},${nextSrc}`);
|
setVideoBase64(addedNode);
|
||||||
|
observer.disconnect(); // Stop observing once the video is handled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
observer.observe(slide, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reveal.on( 'ready', fixBase64VideoBackground );
|
}
|
||||||
Reveal.on( 'slidechanged', fixBase64VideoBackground );
|
// Setup base64 videos
|
||||||
|
Reveal.on( 'ready', fixBase64VideoBackground );
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
# For the full list of built-in configuration values, see the documentation:
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
@ -30,9 +31,11 @@ extensions = [
|
|||||||
# Additional
|
# Additional
|
||||||
"nbsphinx",
|
"nbsphinx",
|
||||||
"myst_parser",
|
"myst_parser",
|
||||||
|
"sphinxcontrib.programoutput",
|
||||||
"sphinxext.opengraph",
|
"sphinxext.opengraph",
|
||||||
"sphinx_click",
|
"sphinx_click",
|
||||||
"sphinx_copybutton",
|
"sphinx_copybutton",
|
||||||
|
"sphinx_design",
|
||||||
# Custom
|
# Custom
|
||||||
"manim_slides.docs.manim_slides_directive",
|
"manim_slides.docs.manim_slides_directive",
|
||||||
]
|
]
|
||||||
@ -53,6 +56,7 @@ add_module_names = False
|
|||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
|
||||||
html_theme = "furo"
|
html_theme = "furo"
|
||||||
html_static_path = ["_static"]
|
html_static_path = ["_static"]
|
||||||
html_favicon = "_static/favicon.png"
|
html_favicon = "_static/favicon.png"
|
||||||
|
@ -24,25 +24,30 @@ the repository, and clone it locally.
|
|||||||
|
|
||||||
As for every Python project, using virtual environment is recommended to avoid
|
As for every Python project, using virtual environment is recommended to avoid
|
||||||
conflicts between modules.
|
conflicts between modules.
|
||||||
For this project, we use [Rye](https://rye-up.com/) 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.
|
and development dependencies. If not already, please install this tool.
|
||||||
|
|
||||||
## Installing Python modules
|
## Installing Python modules
|
||||||
|
|
||||||
With Rye, installation becomes straightforward:
|
With uv, installation becomes straightforward:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye sync --all-features
|
uv sync
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::{note}
|
||||||
|
You still need the same dependencies as to install Manim and ManimGL,
|
||||||
|
so please check their respective installation guides.
|
||||||
|
:::
|
||||||
|
|
||||||
## Running commands
|
## Running commands
|
||||||
|
|
||||||
Because modules are installed in a new Python environment,
|
Because modules are installed in a new Python environment,
|
||||||
you cannot use them directly in the shell.
|
you cannot use them directly in the shell.
|
||||||
Instead, you either need to prepend `rye run` to any command, e.g.:
|
Instead, you either need to prepend `uv run` to any command, e.g.:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye run manim-slides wizard
|
uv run manim-slides wizard
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing your code
|
## Testing your code
|
||||||
@ -51,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:
|
The only command you should run locally is:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye 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
|
This runs a few linter and formatter to make sure the code quality and style stay
|
||||||
@ -61,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:
|
For testing your code, simply run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye run pytest
|
uv run pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the documentation
|
## Building the documentation
|
||||||
@ -73,7 +78,7 @@ To generate the documentation, run the following:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docs
|
cd docs
|
||||||
rye run make html
|
uv run make html
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, the output index file is located at `docs/build/html/index.html` and
|
Then, the output index file is located at `docs/build/html/index.html` and
|
||||||
|
@ -29,13 +29,8 @@ ManimGL support is only guaranteed to work
|
|||||||
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
||||||
and its development is not very active.
|
and its development is not very active.
|
||||||
|
|
||||||
The typical issues are that (1) ManimGL needs an outdated NumPy version
|
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
|
||||||
and (2) ManimGL **should not** be installed from the GitHub repository,
|
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
|
||||||
at least not from the `main` branch, but from a version released to PyPI.
|
|
||||||
|
|
||||||
To solve the NumPy issue, you can safely downgrade NumPy to a version supported
|
|
||||||
by ManimGL,
|
|
||||||
while ignoring the possible *conflicting dependencies* messages from `pip` (or else).
|
|
||||||
|
|
||||||
### Presenting
|
### Presenting
|
||||||
|
|
||||||
@ -54,9 +49,12 @@ with ManimCE or ManimGL.
|
|||||||
### Slides go black when video finishes
|
### Slides go black when video finishes
|
||||||
|
|
||||||
This is an issue with Qt,
|
This is an issue with Qt,
|
||||||
which cannot be solve on all platforms and Python versions,
|
which cannot be solved on all platforms and Python versions,
|
||||||
see [#293](https://github.com/jeertmans/manim-slides/issues/293).
|
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
|
### How to increase quality on Windows
|
||||||
|
|
||||||
On Windows platform, one may encounter a lower image resolution than expected.
|
On Windows platform, one may encounter a lower image resolution than expected.
|
||||||
@ -104,7 +102,7 @@ Questions related to `manim-slides convert [SCENES]... output.html`.
|
|||||||
|
|
||||||
### I moved my `.html` file and it stopped working
|
### I moved my `.html` file and it stopped working
|
||||||
|
|
||||||
If you did not specify `-cdata_uri=true` when converting,
|
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
|
||||||
then Manim Slides generated a folder containing all
|
then Manim Slides generated a folder containing all
|
||||||
the video files, in the same folder as the HTML
|
the video files, in the same folder as the HTML
|
||||||
output. As the path to video files is a relative path,
|
output. As the path to video files is a relative path,
|
||||||
@ -120,3 +118,7 @@ This issue is (probably) caused by PowerPoint never freeing
|
|||||||
memory, causing memory allocation errors, and can be partially
|
memory, causing memory allocation errors, and can be partially
|
||||||
solved by reducing the video quality or the number of slides,
|
solved by reducing the video quality or the number of slides,
|
||||||
see [#392](https://github.com/jeertmans/manim-slides/issues/392).
|
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.
|
||||||
|
@ -10,20 +10,22 @@ The following summarizes the different presentation features Manim Slides offers
|
|||||||
| :--- | :---: | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: | :---: |
|
||||||
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
|
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
|
||||||
| Replay slide | Yes | No | No | N/A |
|
| Replay slide | Yes | No | No | N/A |
|
||||||
| Pause animation | Yes | No | No | N/A |
|
| Pause animation | Yes | Yes | No | N/A |
|
||||||
| Play slide in reverse | Yes | No | No | N/A |
|
| Play slide in reverse | Yes | No | No | N/A |
|
||||||
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
|
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
|
||||||
| Needs Python with Manim Slides installed | Yes | No | No | No
|
| Needs Python with Manim Slides installed | Yes | No | No | No
|
||||||
| Requires internet access | No | Yes | No | No |
|
| Requires internet access | No | Depends[^1] | No | No |
|
||||||
| Auto. play slides | Yes | Yes | Yes | N/A |
|
| Auto. play slides | Yes | Yes | Yes | N/A |
|
||||||
| Loops support | Yes | Yes | Yes | N/A |
|
| Loops support | Yes | Yes | Yes | N/A |
|
||||||
| Fully customizable | No | Yes (`--use-template` option) | No | No |
|
| Fully customizable | No | Yes (`--use-template` option) | No | No |
|
||||||
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1] | None |
|
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^2] | None |
|
||||||
| Works cross-platforms | Yes | Yes | Partly[^1][^2] | Yes |
|
| Works cross-platforms | Yes | Yes | Partly[^2][^3] | Yes |
|
||||||
:::
|
:::
|
||||||
|
|
||||||
[^1]: If you encounter a problem where slides do not automatically play or loops do not work,
|
[^1]: By default, HTML assets are loaded from the internet, but they can be
|
||||||
|
pre-downloaded and embedded in the HTML file at conversion time.
|
||||||
|
[^2]: If you encounter a problem where slides do not automatically play or loops do not work,
|
||||||
please
|
please
|
||||||
[file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).
|
[file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).
|
||||||
[^2]: PowerPoint online does not seem to support automatic playing of videos,
|
[^3]: PowerPoint online does not seem to support automatic playing of videos,
|
||||||
so you need LibreOffice Impress on Linux platforms.
|
so you need LibreOffice Impress on Linux platforms.
|
||||||
|
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/mtheoretical).
|
||||||
|
|
||||||
|
### 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!*
|
@ -43,6 +43,7 @@ installation
|
|||||||
reference/index
|
reference/index
|
||||||
features_table
|
features_table
|
||||||
manim_or_manimgl
|
manim_or_manimgl
|
||||||
|
gallery
|
||||||
faq
|
faq
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -29,19 +29,6 @@ please refer to their specific installation guidelines:
|
|||||||
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
||||||
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
||||||
|
|
||||||
:::{warning}
|
|
||||||
If you install Manim from its git repository, as suggested by ManimGL,
|
|
||||||
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
|
|
||||||
for ManimGL), otherwise it might install an unsupported version of Manim!
|
|
||||||
See [#314](https://github.com/jeertmans/manim-slides/issues/314).
|
|
||||||
|
|
||||||
Also, note that ManimGL uses outdated dependencies, and may
|
|
||||||
not work out-of-the-box. One example is NumPy: ManimGL
|
|
||||||
does not specify any restriction on this package, but
|
|
||||||
only `numpy<1.25` will work, see
|
|
||||||
[#2053](https://github.com/3b1b/manim/issues/2053).
|
|
||||||
:::
|
|
||||||
|
|
||||||
<!-- end deps -->
|
<!-- end deps -->
|
||||||
|
|
||||||
## Pip Install
|
## Pip Install
|
||||||
@ -108,17 +95,12 @@ using optional dependencies:
|
|||||||
|
|
||||||
- `full`, to include `magic`, `manim`, and
|
- `full`, to include `magic`, `manim`, and
|
||||||
`sphinx-directive` extras (see below);
|
`sphinx-directive` extras (see below);
|
||||||
- `full-gl`, to include `magic`, `manimgl`, and
|
|
||||||
`sphinx-directive` extras (see below);
|
|
||||||
- `magic`, to include a Jupyter magic to render
|
- `magic`, to include a Jupyter magic to render
|
||||||
animations inside notebooks. This automatically installs `manim`,
|
animations inside notebooks. This automatically installs `manim`,
|
||||||
and does not work with ManimGL;
|
and does not work with ManimGL;
|
||||||
- `manim` and `manimgl`, for installing the corresponding
|
- `manim` and `manimgl`, for installing the corresponding
|
||||||
dependencies;
|
dependencies;
|
||||||
- `pyqt6` to include PyQt6 Qt bindings. Those bindings are available
|
- `pyqt6` to include PyQt6 Qt bindings;
|
||||||
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-full` to include `full` and `pyqt6`;
|
- `pyqt6-full` to include `full` and `pyqt6`;
|
||||||
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
|
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
|
||||||
on most platforms and Python version, except on Python 3.12[^2];
|
on most platforms and Python version, except on Python 3.12[^2];
|
||||||
@ -142,12 +124,12 @@ Manim Slides is distributed under Nixpkgs >=24.05.
|
|||||||
If you are using Nix or NixOS, you can find Manim Slides under:
|
If you are using Nix or NixOS, you can find Manim Slides under:
|
||||||
|
|
||||||
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
|
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
|
||||||
includes pyqt6 (see above);
|
includes PyQt6 (see above);
|
||||||
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
||||||
module (for notebook magics), and includes IPython but not does not include
|
module (for notebook magics), and includes IPython but does not include
|
||||||
any Qt bindings.
|
any Qt binding.
|
||||||
|
|
||||||
You can try out the Manim Slides package with
|
You can try out the Manim Slides package with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix-shell -p manim ffmpeg manim-slides
|
nix-shell -p manim ffmpeg manim-slides
|
||||||
@ -165,7 +147,7 @@ nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...
|
|||||||
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
|
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
|
||||||
|
|
||||||
:::{note}
|
:::{note}
|
||||||
Nix current does not support `manimgl`.
|
Nix does not currently support `manimgl`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## When you need a Qt backend
|
## When you need a Qt backend
|
||||||
|
@ -18,6 +18,8 @@ use, not the methods used internally when rendering.
|
|||||||
next_section,
|
next_section,
|
||||||
next_slide,
|
next_slide,
|
||||||
remove_from_canvas,
|
remove_from_canvas,
|
||||||
|
start_skip_animations,
|
||||||
|
stop_skip_animations,
|
||||||
wait_time_between_slides,
|
wait_time_between_slides,
|
||||||
wipe,
|
wipe,
|
||||||
zoom,
|
zoom,
|
||||||
|
@ -8,3 +8,30 @@ This page contains an exhaustive list of all the commands available with `manim-
|
|||||||
:prog: manim-slides
|
:prog: manim-slides
|
||||||
:nested: full
|
:nested: full
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## All config options
|
||||||
|
|
||||||
|
Each converter has its own configuration options, which are listed below.
|
||||||
|
|
||||||
|
::::{dropdown} HTML
|
||||||
|
```{program-output} manim-slides convert --to=html --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
|
||||||
|
::::{dropdown} Zip
|
||||||
|
:::{note}
|
||||||
|
The Zip converter inherits from the HTML converter.
|
||||||
|
:::
|
||||||
|
```{program-output} manim-slides convert --to=zip --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
|
||||||
|
::::{dropdown} PDF
|
||||||
|
```{program-output} manim-slides convert --to=pdf --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
|
||||||
|
::::{dropdown} HTML
|
||||||
|
```{program-output} manim-slides convert --to=pdf --show-config
|
||||||
|
```
|
||||||
|
::::
|
||||||
|
@ -78,9 +78,9 @@
|
|||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"kernelspec": {
|
"kernelspec": {
|
||||||
"display_name": "manim-slides",
|
"display_name": ".venv",
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"name": "manim-slides"
|
"name": "python3"
|
||||||
},
|
},
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"codemirror_mode": {
|
"codemirror_mode": {
|
||||||
@ -92,7 +92,7 @@
|
|||||||
"name": "python",
|
"name": "python",
|
||||||
"nbconvert_exporter": "python",
|
"nbconvert_exporter": "python",
|
||||||
"pygments_lexer": "ipython3",
|
"pygments_lexer": "ipython3",
|
||||||
"version": "3.10.6"
|
"version": "3.11.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
@ -137,9 +137,10 @@ and it there to preserve the original aspect ratio (16:9).
|
|||||||
|
|
||||||
### Sharing ONE HTML file
|
### Sharing ONE HTML file
|
||||||
|
|
||||||
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
|
If you set the `--one-file` flag, all animations will be data URI encoded,
|
||||||
all animations will be data URI encoded, making the HTML a self-contained
|
making the HTML a self-contained presentation file that can be shared
|
||||||
presentation file that can be shared on its own.
|
on its own. If you also set the `--offline` flag, the JS and CSS files will
|
||||||
|
be included in the HTML file as well.
|
||||||
|
|
||||||
### Over the internet
|
### Over the internet
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ Pages. Please refer to the template page for usage instructions.
|
|||||||
|
|
||||||
### With PowerPoint (*EXPERIMENTAL*)
|
### 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,
|
`python-pptx` package. Even though it is fully working,
|
||||||
it is still considered in an *EXPERIMENTAL* status because we do not
|
it is still considered in an *EXPERIMENTAL* status because we do not
|
||||||
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
|
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
|
||||||
|
20
example.py
20
example.py
@ -40,7 +40,7 @@ class ConvertExample(Slide):
|
|||||||
step_2 = Text("2. Replace Scene with Slide")
|
step_2 = Text("2. Replace Scene with Slide")
|
||||||
step_3 = Text("3. In construct, add pauses where you need")
|
step_3 = Text("3. In construct, add pauses where you need")
|
||||||
step_4 = Text("4. You can also create loops")
|
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")
|
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]:
|
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
|
||||||
@ -53,7 +53,7 @@ class ConvertExample(Slide):
|
|||||||
self.next_slide()
|
self.next_slide()
|
||||||
|
|
||||||
code = Code(
|
code = Code(
|
||||||
code="""from manim import *
|
code_string="""from manim import *
|
||||||
|
|
||||||
|
|
||||||
class Example(Scene):
|
class Example(Scene):
|
||||||
@ -72,7 +72,7 @@ class Example(Scene):
|
|||||||
)
|
)
|
||||||
|
|
||||||
code_step_1 = Code(
|
code_step_1 = Code(
|
||||||
code="""from manim import *
|
code_string="""from manim import *
|
||||||
from manim_slides import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
class Example(Scene):
|
class Example(Scene):
|
||||||
@ -91,7 +91,7 @@ class Example(Scene):
|
|||||||
)
|
)
|
||||||
|
|
||||||
code_step_2 = Code(
|
code_step_2 = Code(
|
||||||
code="""from manim import *
|
code_string="""from manim import *
|
||||||
from manim_slides import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
class Example(Slide):
|
class Example(Slide):
|
||||||
@ -110,7 +110,7 @@ class Example(Slide):
|
|||||||
)
|
)
|
||||||
|
|
||||||
code_step_3 = Code(
|
code_step_3 = Code(
|
||||||
code="""from manim import *
|
code_string="""from manim import *
|
||||||
from manim_slides import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
class Example(Slide):
|
class Example(Slide):
|
||||||
@ -129,7 +129,7 @@ class Example(Slide):
|
|||||||
)
|
)
|
||||||
|
|
||||||
code_step_4 = Code(
|
code_step_4 = Code(
|
||||||
code="""from manim import *
|
code_string="""from manim import *
|
||||||
from manim_slides import Slide
|
from manim_slides import Slide
|
||||||
|
|
||||||
class Example(Slide):
|
class Example(Slide):
|
||||||
@ -148,19 +148,19 @@ class Example(Slide):
|
|||||||
)
|
)
|
||||||
|
|
||||||
code_step_5 = Code(
|
code_step_5 = Code(
|
||||||
code="manim example.py Example",
|
code_string="manim-slide render example.py Example",
|
||||||
language="console",
|
language="console",
|
||||||
)
|
)
|
||||||
|
|
||||||
code_step_6 = Code(
|
code_step_6 = Code(
|
||||||
code="manim-slides Example",
|
code_string="manim-slides Example",
|
||||||
language="console",
|
language="console",
|
||||||
)
|
)
|
||||||
|
|
||||||
or_text = Text("or generate HTML presentation").scale(0.5)
|
or_text = Text("or generate HTML presentation").scale(0.5)
|
||||||
|
|
||||||
code_step_7 = Code(
|
code_step_7 = Code(
|
||||||
code="manim-slides convert Example slides.html --open",
|
code_string="manim-slides convert Example slides.html --open",
|
||||||
language="console",
|
language="console",
|
||||||
).shift(DOWN)
|
).shift(DOWN)
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ class Example(Slide):
|
|||||||
self.play(Transform(code, code_step_6))
|
self.play(Transform(code, code_step_6))
|
||||||
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
|
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.next_slide(loop=True)
|
||||||
self.play(FadeIn(watch_text))
|
self.play(FadeIn(watch_text))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, List
|
from typing import Any
|
||||||
|
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import requests
|
|||||||
from click_default_group import DefaultGroup
|
from click_default_group import DefaultGroup
|
||||||
|
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
|
from .checkhealth import checkhealth
|
||||||
from .convert import convert
|
from .convert import convert
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .present import list_scenes, present
|
from .present import list_scenes, present
|
||||||
@ -63,6 +64,7 @@ def cli(notify_outdated_version: bool) -> None:
|
|||||||
|
|
||||||
|
|
||||||
cli.add_command(convert)
|
cli.add_command(convert)
|
||||||
|
cli.add_command(checkhealth)
|
||||||
cli.add_command(init)
|
cli.add_command(init)
|
||||||
cli.add_command(list_scenes)
|
cli.add_command(list_scenes)
|
||||||
cli.add_command(present)
|
cli.add_command(present)
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "5.1.5"
|
__version__ = "5.5.1"
|
||||||
|
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:
|
def folder_path_option(function: F) -> F:
|
||||||
"""Wrap a function to add folder path option."""
|
"""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(
|
wrapper: Wrapper = click.option(
|
||||||
"--folder",
|
"--folder",
|
||||||
metavar="DIRECTORY",
|
metavar="DIRECTORY",
|
||||||
default=FOLDER_PATH,
|
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.",
|
help="Set slides folder.",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
)
|
)
|
||||||
|
@ -13,6 +13,8 @@ from pydantic import (
|
|||||||
FilePath,
|
FilePath,
|
||||||
PositiveInt,
|
PositiveInt,
|
||||||
PrivateAttr,
|
PrivateAttr,
|
||||||
|
conset,
|
||||||
|
field_serializer,
|
||||||
field_validator,
|
field_validator,
|
||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
@ -47,20 +49,13 @@ def key_id(name: str) -> PositiveInt:
|
|||||||
class Key(BaseModel): # type: ignore[misc]
|
class Key(BaseModel): # type: ignore[misc]
|
||||||
"""Represents a list of key codes, with optionally a name."""
|
"""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
|
name: Optional[str] = None
|
||||||
|
|
||||||
__signal: Signal = PrivateAttr(default_factory=Signal)
|
__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:
|
def set_ids(self, *ids: int) -> None:
|
||||||
self.ids = list(set(ids))
|
self.ids = set(ids)
|
||||||
|
|
||||||
def match(self, key_id: int) -> bool:
|
def match(self, key_id: int) -> bool:
|
||||||
m = key_id in self.ids
|
m = key_id in self.ids
|
||||||
@ -77,6 +72,10 @@ class Key(BaseModel): # type: ignore[misc]
|
|||||||
def connect(self, function: Receiver) -> None:
|
def connect(self, function: Receiver) -> None:
|
||||||
self.__signal.connect(function)
|
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]
|
class Keys(BaseModel): # type: ignore[misc]
|
||||||
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
|
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
|
||||||
@ -161,6 +160,8 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
reversed_playback_rate: float = 1.0
|
reversed_playback_rate: float = 1.0
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
dedent_notes: bool = True
|
dedent_notes: bool = True
|
||||||
|
skip_animations: bool = False
|
||||||
|
src: Optional[FilePath] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
|
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
|
||||||
@ -180,7 +181,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
fun_kwargs = {
|
fun_kwargs = {
|
||||||
key: value
|
key: value
|
||||||
for key, value in kwargs.items()
|
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)
|
fun_kwargs[arg_name] = cls(**kwargs)
|
||||||
return fun(*args, **fun_kwargs)
|
return fun(*args, **fun_kwargs)
|
||||||
@ -194,7 +195,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
default=field_info.default,
|
default=field_info.default,
|
||||||
annotation=field_info.annotation,
|
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)
|
sig = sig.replace(parameters=parameters)
|
||||||
@ -205,14 +206,13 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
return _wrapper_
|
return _wrapper_
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
|
||||||
def apply_dedent_notes(
|
def apply_dedent_notes(
|
||||||
cls, base_slide_config: "BaseSlideConfig"
|
self,
|
||||||
) -> "BaseSlideConfig":
|
) -> "BaseSlideConfig":
|
||||||
if base_slide_config.dedent_notes:
|
if self.dedent_notes:
|
||||||
base_slide_config.notes = dedent(base_slide_config.notes)
|
self.notes = dedent(self.notes)
|
||||||
|
|
||||||
return base_slide_config
|
return self
|
||||||
|
|
||||||
|
|
||||||
class PreSlideConfig(BaseSlideConfig):
|
class PreSlideConfig(BaseSlideConfig):
|
||||||
@ -231,7 +231,7 @@ class PreSlideConfig(BaseSlideConfig):
|
|||||||
return cls(
|
return cls(
|
||||||
start_animation=start_animation,
|
start_animation=start_animation,
|
||||||
end_animation=end_animation,
|
end_animation=end_animation,
|
||||||
**base_slide_config.dict(),
|
**base_slide_config.model_dump(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator("start_animation", "end_animation")
|
@field_validator("start_animation", "end_animation")
|
||||||
@ -242,40 +242,33 @@ class PreSlideConfig(BaseSlideConfig):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
|
||||||
def start_animation_is_before_end(
|
def start_animation_is_before_end(
|
||||||
cls, pre_slide_config: "PreSlideConfig"
|
self,
|
||||||
) -> "PreSlideConfig":
|
) -> "PreSlideConfig":
|
||||||
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
|
if self.start_animation > self.end_animation:
|
||||||
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
|
|
||||||
raise ValueError(
|
|
||||||
"You have to play at least one animation (e.g., `self.wait()`) "
|
|
||||||
"before pausing. If you want to start paused, use the appropriate "
|
|
||||||
"command-line option when presenting. "
|
|
||||||
"IMPORTANT: when using ManimGL, `self.wait()` is not considered "
|
|
||||||
"to be an animation, so prefer to directly use `self.play(...)`."
|
|
||||||
)
|
|
||||||
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Start animation index must be strictly lower than end animation index"
|
"Start animation index must be strictly lower than end animation index"
|
||||||
)
|
)
|
||||||
|
return self
|
||||||
return pre_slide_config
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
def has_src_or_more_than_zero_animations(
|
||||||
def loop_and_auto_next_disallowed(
|
self,
|
||||||
cls, pre_slide_config: "PreSlideConfig"
|
|
||||||
) -> "PreSlideConfig":
|
) -> "PreSlideConfig":
|
||||||
if pre_slide_config.loop and pre_slide_config.auto_next:
|
if self.src is not None and self.start_animation != self.end_animation:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"You cannot have both `loop=True` and `auto_next=True`, "
|
"A slide cannot have 'src=...' and more than zero animations at the same time."
|
||||||
"because a looping slide has no ending. "
|
)
|
||||||
"This may be supported in the future if "
|
elif self.src is None and self.start_animation == self.end_animation:
|
||||||
"https://github.com/jeertmans/manim-slides/pull/299 gets merged."
|
raise ValueError(
|
||||||
|
"You have to play at least one animation (e.g., 'self.wait()') "
|
||||||
|
"before pausing. If you want to start paused, use the appropriate "
|
||||||
|
"command-line option when presenting. "
|
||||||
|
"IMPORTANT: when using ManimGL, 'self.wait()' is not considered "
|
||||||
|
"to be an animation, so prefer to directly use 'self.play(...)'."
|
||||||
)
|
)
|
||||||
|
|
||||||
return pre_slide_config
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slides_slice(self) -> slice:
|
def slides_slice(self) -> slice:
|
||||||
@ -292,7 +285,7 @@ class SlideConfig(BaseSlideConfig):
|
|||||||
def from_pre_slide_config_and_files(
|
def from_pre_slide_config_and_files(
|
||||||
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
||||||
) -> "SlideConfig":
|
) -> "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]
|
class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||||
@ -324,23 +317,22 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
|||||||
f.write(self.model_dump_json(indent=2))
|
f.write(self.model_dump_json(indent=2))
|
||||||
|
|
||||||
def copy_to(
|
def copy_to(
|
||||||
self, folder: Path, use_cached: bool = True, include_reversed: bool = True
|
self,
|
||||||
) -> "PresentationConfig":
|
folder: Path,
|
||||||
|
use_cached: bool = True,
|
||||||
|
include_reversed: bool = True,
|
||||||
|
prefix: str = "",
|
||||||
|
) -> None:
|
||||||
"""Copy the files to a given directory."""
|
"""Copy the files to a given directory."""
|
||||||
for slide_config in self.slides:
|
for slide_config in self.slides:
|
||||||
file = slide_config.file
|
file = slide_config.file
|
||||||
rev_file = slide_config.rev_file
|
rev_file = slide_config.rev_file
|
||||||
|
|
||||||
dest = folder / file.name
|
dest = folder / f"{prefix}{file.name}"
|
||||||
rev_dest = folder / rev_file.name
|
rev_dest = folder / f"{prefix}{rev_file.name}"
|
||||||
|
|
||||||
slide_config.file = dest
|
|
||||||
slide_config.rev_file = rev_dest
|
|
||||||
|
|
||||||
if not use_cached or not dest.exists():
|
if not use_cached or not dest.exists():
|
||||||
shutil.copy(file, dest)
|
shutil.copy(file, dest)
|
||||||
|
|
||||||
if include_reversed and (not use_cached or not rev_dest.exists()):
|
if include_reversed and (not use_cached or not rev_dest.exists()):
|
||||||
shutil.copy(rev_file, rev_dest)
|
shutil.copy(rev_file, rev_dest)
|
||||||
|
|
||||||
return self
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
import warnings
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@ -14,6 +17,8 @@ from typing import Any, Callable, Optional, Union
|
|||||||
import av
|
import av
|
||||||
import click
|
import click
|
||||||
import pptx
|
import pptx
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from click import Context, Parameter
|
from click import Context, Parameter
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@ -27,7 +32,6 @@ from pydantic import (
|
|||||||
PositiveFloat,
|
PositiveFloat,
|
||||||
PositiveInt,
|
PositiveInt,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
conlist,
|
|
||||||
)
|
)
|
||||||
from pydantic_core import CoreSchema, core_schema
|
from pydantic_core import CoreSchema, core_schema
|
||||||
from pydantic_extra_types.color import Color
|
from pydantic_extra_types.color import Color
|
||||||
@ -99,9 +103,12 @@ def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
|
|||||||
|
|
||||||
|
|
||||||
class Converter(BaseModel): # type: ignore
|
class Converter(BaseModel): # type: ignore
|
||||||
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
|
presentation_configs: list[PresentationConfig]
|
||||||
assets_dir: str = "{basename}_assets"
|
assets_dir: str = Field(
|
||||||
template: Optional[Path] = None
|
"{basename}_assets",
|
||||||
|
description="Assets folder.\nThis is a template string that accepts 'dirname', 'basename', and 'ext' as variables.\nThose variables are obtained from the output filename.",
|
||||||
|
)
|
||||||
|
template: Optional[Path] = Field(None, description="Custom template file to use.")
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None:
|
||||||
"""Convert self, i.e., a list of presentations, into a given format."""
|
"""Convert self, i.e., a list of presentations, into a given format."""
|
||||||
@ -115,9 +122,9 @@ class Converter(BaseModel): # type: ignore
|
|||||||
"""
|
"""
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def open(self, file: Path) -> Any:
|
def open(self, file: Path) -> None:
|
||||||
"""Open a file, generated with converter, using appropriate application."""
|
"""Open a file, generated with converter, using appropriate application."""
|
||||||
raise NotImplementedError
|
open_with_default(file)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, s: str) -> type["Converter"]:
|
def from_string(cls, s: str) -> type["Converter"]:
|
||||||
@ -126,6 +133,7 @@ class Converter(BaseModel): # type: ignore
|
|||||||
"html": RevealJS,
|
"html": RevealJS,
|
||||||
"pdf": PDF,
|
"pdf": PDF,
|
||||||
"pptx": PowerPoint,
|
"pptx": PowerPoint,
|
||||||
|
"zip": HtmlZip,
|
||||||
}[s]
|
}[s]
|
||||||
|
|
||||||
|
|
||||||
@ -285,49 +293,153 @@ class RevealTheme(str, StrEnum):
|
|||||||
|
|
||||||
|
|
||||||
class RevealJS(Converter):
|
class RevealJS(Converter):
|
||||||
# Export option: use data-uri
|
"""
|
||||||
data_uri: bool = False
|
RevealJS options.
|
||||||
|
|
||||||
|
Please check out https://revealjs.com/config/ for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Export option:
|
||||||
|
one_file: bool = Field(
|
||||||
|
False, description="Embed all assets (e.g., animations) inside the HTML."
|
||||||
|
)
|
||||||
|
offline: bool = Field(
|
||||||
|
False, description="Download remote assets for offline presentation."
|
||||||
|
)
|
||||||
# Presentation size options from RevealJS
|
# Presentation size options from RevealJS
|
||||||
width: Union[Str, int] = Str("100%")
|
width: Union[Str, int] = Field(
|
||||||
height: Union[Str, int] = Str("100%")
|
Str("100%"), description="Width of the presentation."
|
||||||
margin: float = 0.04
|
)
|
||||||
min_scale: float = 0.2
|
height: Union[Str, int] = Field(
|
||||||
max_scale: float = 2.0
|
Str("100%"), description="Height of the presentation."
|
||||||
|
)
|
||||||
|
margin: float = Field(0.04, description="Margin to use around the content.")
|
||||||
|
min_scale: float = Field(
|
||||||
|
0.2, description="Bound for smallest possible scale to apply to content."
|
||||||
|
)
|
||||||
|
max_scale: float = Field(
|
||||||
|
2.0, description="Bound for large possible scale to apply to content."
|
||||||
|
)
|
||||||
# Configuration options from RevealJS
|
# Configuration options from RevealJS
|
||||||
controls: JsBool = JsBool.false
|
controls: JsBool = Field(
|
||||||
controls_tutorial: JsBool = JsBool.true
|
JsBool.false, description="Display presentation control arrows."
|
||||||
controls_layout: ControlsLayout = ControlsLayout.bottom_right
|
)
|
||||||
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
|
controls_tutorial: JsBool = Field(
|
||||||
progress: JsBool = JsBool.false
|
JsBool.true, description="Help the user learn the controls by providing hints."
|
||||||
slide_number: SlideNumber = SlideNumber.false
|
)
|
||||||
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
|
controls_layout: ControlsLayout = Field(
|
||||||
hash_one_based_index: JsBool = JsBool.false
|
ControlsLayout.bottom_right, description="Determine where controls appear."
|
||||||
hash: JsBool = JsBool.false
|
)
|
||||||
respond_to_hash_changes: JsBool = JsBool.false
|
controls_back_arrows: ControlsBackArrows = Field(
|
||||||
history: JsBool = JsBool.false
|
ControlsBackArrows.faded,
|
||||||
keyboard: JsBool = JsBool.true
|
description="Visibility rule for backwards navigation arrows.",
|
||||||
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
|
)
|
||||||
disable_layout: JsBool = JsBool.false
|
progress: JsBool = Field(
|
||||||
overview: JsBool = JsBool.true
|
JsBool.false, description="Display a presentation progress bar."
|
||||||
center: JsBool = JsBool.true
|
)
|
||||||
touch: JsBool = JsBool.true
|
slide_number: SlideNumber = Field(
|
||||||
loop: JsBool = JsBool.false
|
SlideNumber.false, description="Display the page number of the current slide."
|
||||||
rtl: JsBool = JsBool.false
|
)
|
||||||
navigation_mode: NavigationMode = NavigationMode.default
|
show_slide_number: Union[ShowSlideNumber, Function] = Field(
|
||||||
shuffle: JsBool = JsBool.false
|
ShowSlideNumber.all,
|
||||||
fragments: JsBool = JsBool.true
|
description="Can be used to limit the contexts in which the slide number appears.",
|
||||||
fragment_in_url: JsBool = JsBool.true
|
)
|
||||||
embedded: JsBool = JsBool.false
|
hash_one_based_index: JsBool = Field(
|
||||||
help: JsBool = JsBool.true
|
JsBool.false,
|
||||||
pause: JsBool = JsBool.true
|
description="Use 1 based indexing for # links to match slide number (default is zero based).",
|
||||||
show_notes: JsBool = JsBool.false
|
)
|
||||||
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
|
hash: JsBool = Field(
|
||||||
preload_iframes: PreloadIframes = PreloadIframes.null
|
JsBool.false,
|
||||||
auto_animate: JsBool = JsBool.true
|
description="Add the current slide number to the URL hash so that reloading the page/copying the URL will return you to the same slide.",
|
||||||
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
|
)
|
||||||
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
|
respond_to_hash_changes: JsBool = Field(
|
||||||
auto_animate_duration: float = 1.0
|
JsBool.false,
|
||||||
auto_animate_unmatched: JsBool = JsBool.true
|
description="Flags if we should monitor the hash and change slides accordingly.",
|
||||||
|
)
|
||||||
|
jump_to_slide: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Enable support for jump-to-slide navigation shortcuts.",
|
||||||
|
)
|
||||||
|
history: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Push each slide change to the browser history. Implies `hash: true`.",
|
||||||
|
)
|
||||||
|
keyboard: JsBool = Field(
|
||||||
|
JsBool.true, description="Enable keyboard shortcuts for navigation."
|
||||||
|
)
|
||||||
|
keyboard_condition: Union[KeyboardCondition, Function] = Field(
|
||||||
|
KeyboardCondition.null,
|
||||||
|
description="Optional function that blocks keyboard events when retuning false.",
|
||||||
|
)
|
||||||
|
disable_layout: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Disable the default reveal.js slide layout (scaling and centering) so that you can use custom CSS layout.",
|
||||||
|
)
|
||||||
|
overview: JsBool = Field(JsBool.true, description="Enable the slide overview mode.")
|
||||||
|
center: JsBool = Field(JsBool.true, description="Vertical centering of slides.")
|
||||||
|
touch: JsBool = Field(
|
||||||
|
JsBool.true, description="Enable touch navigation on devices with touch input."
|
||||||
|
)
|
||||||
|
loop: JsBool = Field(JsBool.false, description="Loop the presentation.")
|
||||||
|
rtl: JsBool = Field(
|
||||||
|
JsBool.false, description="Change the presentation direction to be RTL."
|
||||||
|
)
|
||||||
|
navigation_mode: NavigationMode = Field(
|
||||||
|
NavigationMode.default,
|
||||||
|
description="Change the behavior of our navigation directions.",
|
||||||
|
)
|
||||||
|
shuffle: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Randomize the order of slides each time the presentation loads.",
|
||||||
|
)
|
||||||
|
fragments: JsBool = Field(
|
||||||
|
JsBool.true, description="Turns fragment on and off globally."
|
||||||
|
)
|
||||||
|
fragment_in_url: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Flag whether to include the current fragment in the URL, so that reloading brings you to the same fragment position.",
|
||||||
|
)
|
||||||
|
embedded: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Flag if the presentation is running in an embedded mode, i.e. contained within a limited portion of the screen.",
|
||||||
|
)
|
||||||
|
help: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Flag if we should show a help overlay when the question-mark key is pressed.",
|
||||||
|
)
|
||||||
|
pause: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Flag if it should be possible to pause the presentation (blackout).",
|
||||||
|
)
|
||||||
|
show_notes: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Flag if speaker notes should be visible to all viewers.",
|
||||||
|
)
|
||||||
|
auto_play_media: AutoPlayMedia = Field(
|
||||||
|
AutoPlayMedia.null,
|
||||||
|
description="Global override for autolaying embedded media (video/audio/iframe).",
|
||||||
|
)
|
||||||
|
preload_iframes: PreloadIframes = Field(
|
||||||
|
PreloadIframes.null,
|
||||||
|
description="Global override for preloading lazy-loaded iframes.",
|
||||||
|
)
|
||||||
|
auto_animate: JsBool = Field(
|
||||||
|
JsBool.true, description="Can be used to globally disable auto-animation."
|
||||||
|
)
|
||||||
|
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = Field(
|
||||||
|
AutoAnimateMatcher.null,
|
||||||
|
description="Optionally provide a custom element matcher that will be used to dictate which elements we can animate between.",
|
||||||
|
)
|
||||||
|
auto_animate_easing: AutoAnimateEasing = Field(
|
||||||
|
AutoAnimateEasing.ease,
|
||||||
|
description="Default settings for our auto-animate transitions, can be overridden per-slide or per-element via data arguments.",
|
||||||
|
)
|
||||||
|
auto_animate_duration: float = Field(
|
||||||
|
1.0, description="See 'auto_animate_easing' documentation."
|
||||||
|
)
|
||||||
|
auto_animate_unmatched: JsBool = Field(
|
||||||
|
JsBool.true, description="See 'auto_animate_easing' documentation."
|
||||||
|
)
|
||||||
auto_animate_styles: list[str] = Field(
|
auto_animate_styles: list[str] = Field(
|
||||||
default_factory=lambda: [
|
default_factory=lambda: [
|
||||||
"opacity",
|
"opacity",
|
||||||
@ -342,34 +454,88 @@ class RevealJS(Converter):
|
|||||||
"border-radius",
|
"border-radius",
|
||||||
"outline",
|
"outline",
|
||||||
"outline-offset",
|
"outline-offset",
|
||||||
]
|
],
|
||||||
|
description="CSS properties that can be auto-animated.",
|
||||||
|
)
|
||||||
|
auto_slide: AutoSlide = Field(
|
||||||
|
0, description="Control automatic progression to the next slide."
|
||||||
|
)
|
||||||
|
auto_slide_stoppable: JsBool = Field(
|
||||||
|
JsBool.true, description="Stop auto-sliding after user input."
|
||||||
|
)
|
||||||
|
auto_slide_method: Union[AutoSlideMethod, Function] = Field(
|
||||||
|
AutoSlideMethod.null,
|
||||||
|
description="Use this method for navigation when auto-sliding (defaults to navigateNext).",
|
||||||
|
)
|
||||||
|
default_timing: Union[JsNull, int] = Field(
|
||||||
|
JsNull.null,
|
||||||
|
description="Specify the average time in seconds that you think you will spend presenting each slide.",
|
||||||
|
)
|
||||||
|
mouse_wheel: JsBool = Field(
|
||||||
|
JsBool.false, description="Enable slide navigation via mouse wheel."
|
||||||
|
)
|
||||||
|
preview_links: JsBool = Field(
|
||||||
|
JsBool.false, description="Open links in an iframe preview overlay."
|
||||||
|
)
|
||||||
|
post_message: JsBool = Field(
|
||||||
|
JsBool.true, description="Expose the reveal.js API through window.postMessage."
|
||||||
|
)
|
||||||
|
post_message_events: JsBool = Field(
|
||||||
|
JsBool.false,
|
||||||
|
description="Dispatch all reveal.js events to the parent window through postMessage.",
|
||||||
|
)
|
||||||
|
focus_body_on_page_visibility_change: JsBool = Field(
|
||||||
|
JsBool.true,
|
||||||
|
description="Focus body when page changes visibility to ensure keyboard shortcuts work.",
|
||||||
|
)
|
||||||
|
transition: Transition = Field(Transition.none, description="Transition style.")
|
||||||
|
transition_speed: TransitionSpeed = Field(
|
||||||
|
TransitionSpeed.default, description="Transition speed."
|
||||||
|
)
|
||||||
|
background_size: BackgroundSize = Field(
|
||||||
|
BackgroundSize.contain, description="Background size attribute for each video."
|
||||||
|
) # Not in RevealJS
|
||||||
|
background_transition: BackgroundTransition = Field(
|
||||||
|
BackgroundTransition.none,
|
||||||
|
description="Transition style for full page slide backgrounds.",
|
||||||
|
)
|
||||||
|
pdf_max_pages_per_slide: Union[int, str] = Field(
|
||||||
|
"Number.POSITIVE_INFINITY",
|
||||||
|
description="The maximum number of pages a single slide can expand onto when printing to PDF, unlimited by default.",
|
||||||
|
)
|
||||||
|
pdf_separate_fragments: JsBool = Field(
|
||||||
|
JsBool.true, description="Print each fragment on a separate slide."
|
||||||
|
)
|
||||||
|
pdf_page_height_offset: int = Field(
|
||||||
|
-1,
|
||||||
|
description="Offset used to reduce the height of content within exported PDF pages.",
|
||||||
|
)
|
||||||
|
view_distance: int = Field(
|
||||||
|
3, description="Number of slides away from the current that are visible."
|
||||||
|
)
|
||||||
|
mobile_view_distance: int = Field(
|
||||||
|
2,
|
||||||
|
description="Number of slides away from the current that are visible on mobile devices.",
|
||||||
|
)
|
||||||
|
display: Display = Field(
|
||||||
|
Display.block, description="The display mode that will be used to show slides."
|
||||||
|
)
|
||||||
|
hide_inactive_cursor: JsBool = Field(
|
||||||
|
JsBool.true, description="Hide cursor if inactive."
|
||||||
|
)
|
||||||
|
hide_cursor_time: int = Field(
|
||||||
|
5000, description="Time before the cursor is hidden (in ms)."
|
||||||
)
|
)
|
||||||
auto_slide: AutoSlide = 0
|
|
||||||
auto_slide_stoppable: JsBool = JsBool.true
|
|
||||||
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
|
|
||||||
default_timing: Union[JsNull, int] = JsNull.null
|
|
||||||
mouse_wheel: JsBool = JsBool.false
|
|
||||||
preview_links: JsBool = JsBool.false
|
|
||||||
post_message: JsBool = JsBool.true
|
|
||||||
post_message_events: JsBool = JsBool.false
|
|
||||||
focus_body_on_page_visibility_change: JsBool = JsBool.true
|
|
||||||
transition: Transition = Transition.none
|
|
||||||
transition_speed: TransitionSpeed = TransitionSpeed.default
|
|
||||||
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
|
|
||||||
background_transition: BackgroundTransition = BackgroundTransition.none
|
|
||||||
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
|
|
||||||
pdf_separate_fragments: JsBool = JsBool.true
|
|
||||||
pdf_page_height_offset: int = -1
|
|
||||||
view_distance: int = 3
|
|
||||||
mobile_view_distance: int = 2
|
|
||||||
display: Display = Display.block
|
|
||||||
hide_inactive_cursor: JsBool = JsBool.true
|
|
||||||
hide_cursor_time: int = 5000
|
|
||||||
# Appearance options from RevealJS
|
# Appearance options from RevealJS
|
||||||
background_color: Color = "black"
|
background_color: Color = Field(
|
||||||
reveal_version: str = "5.1.0"
|
"black",
|
||||||
reveal_theme: RevealTheme = RevealTheme.black
|
description="Background color used in slides, not relevant if videos fill the whole area.",
|
||||||
title: str = "Manim Slides"
|
)
|
||||||
|
reveal_version: str = Field("5.2.0", description="RevealJS version.")
|
||||||
|
reveal_theme: RevealTheme = Field(
|
||||||
|
RevealTheme.black, description="RevealJS version."
|
||||||
|
)
|
||||||
|
title: str = Field("Manim Slides", description="Presentation title.")
|
||||||
# Pydantic options
|
# Pydantic options
|
||||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||||
|
|
||||||
@ -380,40 +546,62 @@ class RevealJS(Converter):
|
|||||||
|
|
||||||
return resources.files(templates).joinpath("revealjs.html").read_text()
|
return resources.files(templates).joinpath("revealjs.html").read_text()
|
||||||
|
|
||||||
def open(self, file: Path) -> bool:
|
def open(self, file: Path) -> None:
|
||||||
return webbrowser.open(file.absolute().as_uri())
|
webbrowser.open(file.absolute().as_uri())
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None: # noqa: C901
|
||||||
"""
|
"""
|
||||||
Convert this configuration into a RevealJS HTML presentation, saved to
|
Convert this configuration into a RevealJS HTML presentation, saved to
|
||||||
DEST.
|
DEST.
|
||||||
"""
|
"""
|
||||||
if self.data_uri:
|
dirname = dest.parent
|
||||||
assets_dir = Path("") # Actually we won't care.
|
basename = dest.stem
|
||||||
else:
|
ext = dest.suffix
|
||||||
dirname = dest.parent
|
|
||||||
basename = dest.stem
|
|
||||||
ext = dest.suffix
|
|
||||||
|
|
||||||
assets_dir = Path(
|
assets_dir = Path(
|
||||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||||
)
|
)
|
||||||
full_assets_dir = dirname / assets_dir
|
full_assets_dir = dirname / assets_dir
|
||||||
|
|
||||||
|
if not self.one_file or self.offline:
|
||||||
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
||||||
|
|
||||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
if not self.one_file:
|
||||||
|
num_presentation_configs = len(self.presentation_configs)
|
||||||
|
|
||||||
for presentation_config in self.presentation_configs:
|
if num_presentation_configs > 1:
|
||||||
presentation_config.copy_to(full_assets_dir, include_reversed=False)
|
# 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 ""
|
||||||
|
|
||||||
|
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
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)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with open(dest, "w") as f:
|
with open(dest, "w") as f:
|
||||||
revealjs_template = Template(self.load_template())
|
revealjs_template = Template(
|
||||||
|
self.load_template(), trim_blocks=True, lstrip_blocks=True
|
||||||
|
)
|
||||||
|
|
||||||
options = self.dict()
|
options = self.model_dump()
|
||||||
options["assets_dir"] = assets_dir
|
|
||||||
|
if assets_dir is not None:
|
||||||
|
options["assets_dir"] = assets_dir
|
||||||
|
|
||||||
has_notes = any(
|
has_notes = any(
|
||||||
slide_config.notes != ""
|
slide_config.notes != ""
|
||||||
@ -426,25 +614,89 @@ class RevealJS(Converter):
|
|||||||
get_duration_ms=get_duration_ms,
|
get_duration_ms=get_duration_ms,
|
||||||
has_notes=has_notes,
|
has_notes=has_notes,
|
||||||
env=os.environ,
|
env=os.environ,
|
||||||
|
prefix=prefix if not self.one_file else None,
|
||||||
**options,
|
**options,
|
||||||
)
|
)
|
||||||
|
# If not offline, write the content to the file
|
||||||
|
if not self.offline:
|
||||||
|
f.write(content)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If offline, download remote assets and store them in the assets folder
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
for tag, inner in [("link", "href"), ("script", "src")]:
|
||||||
|
for item in soup.find_all(tag):
|
||||||
|
if item.has_attr(inner) and (link := item[inner]).startswith(
|
||||||
|
"http"
|
||||||
|
):
|
||||||
|
asset_name = link.rsplit("/", 1)[1]
|
||||||
|
asset = session.get(link)
|
||||||
|
if self.one_file:
|
||||||
|
# If it is a CSS file, inline it
|
||||||
|
if tag == "link" and "stylesheet" in item["rel"]:
|
||||||
|
item.decompose()
|
||||||
|
style = soup.new_tag("style")
|
||||||
|
style.string = asset.text
|
||||||
|
soup.head.append(style)
|
||||||
|
# If it is a JS file, inline it
|
||||||
|
elif tag == "script":
|
||||||
|
item.decompose()
|
||||||
|
script = soup.new_tag("script")
|
||||||
|
script.string = asset.text
|
||||||
|
soup.head.append(script)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unable to inline {tag} asset: {link}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(full_assets_dir / asset_name, "wb") as asset_file:
|
||||||
|
asset_file.write(asset.content)
|
||||||
|
|
||||||
|
item[inner] = str(assets_dir / asset_name)
|
||||||
|
|
||||||
|
content = str(soup)
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class FrameIndex(str, Enum):
|
||||||
first = "first"
|
first = "first"
|
||||||
last = "last"
|
last = "last"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class PDF(Converter):
|
class PDF(Converter):
|
||||||
frame_index: FrameIndex = FrameIndex.last
|
frame_index: FrameIndex = Field(
|
||||||
resolution: PositiveFloat = 100.0
|
FrameIndex.last,
|
||||||
|
description="What frame (first or last) is used to represent each slide.",
|
||||||
|
)
|
||||||
|
resolution: PositiveFloat = Field(
|
||||||
|
100.0, description="Image resolution use for saving frames."
|
||||||
|
)
|
||||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||||
|
|
||||||
def open(self, file: Path) -> None:
|
|
||||||
return open_with_default(file)
|
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None:
|
||||||
"""Convert this configuration into a PDF presentation, saved to DEST."""
|
"""Convert this configuration into a PDF presentation, saved to DEST."""
|
||||||
images = []
|
images = []
|
||||||
@ -471,17 +723,30 @@ class PDF(Converter):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPoint(Converter):
|
class PowerPoint(Converter):
|
||||||
left: PositiveInt = 0
|
left: PositiveInt = Field(
|
||||||
top: PositiveInt = 0
|
0, description="Horizontal offset where the video is placed from left border."
|
||||||
width: PositiveInt = 1280
|
)
|
||||||
height: PositiveInt = 720
|
top: PositiveInt = Field(
|
||||||
auto_play_media: bool = True
|
0, description="Vertical offset where the video is placed from top border."
|
||||||
poster_frame_image: Optional[FilePath] = None
|
)
|
||||||
|
width: PositiveInt = Field(
|
||||||
|
1280,
|
||||||
|
description="Width of the slides.\nThis should match the resolution of the presentation.",
|
||||||
|
)
|
||||||
|
height: PositiveInt = Field(
|
||||||
|
720,
|
||||||
|
description="Height of the slides.\nThis should match the resolution of the presentation.",
|
||||||
|
)
|
||||||
|
auto_play_media: bool = Field(
|
||||||
|
True, description="Automatically play animations when changing slide."
|
||||||
|
)
|
||||||
|
poster_frame_image: Optional[FilePath] = Field(
|
||||||
|
None,
|
||||||
|
description="Optional image to use when animations are not playing.\n"
|
||||||
|
"By default, the first frame of each animation is used.\nThis is important to avoid blinking effects between slides.",
|
||||||
|
)
|
||||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||||
|
|
||||||
def open(self, file: Path) -> None:
|
|
||||||
return open_with_default(file)
|
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None:
|
def convert_to(self, dest: Path) -> None:
|
||||||
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
|
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
|
||||||
prs = pptx.Presentation()
|
prs = pptx.Presentation()
|
||||||
@ -498,7 +763,7 @@ class PowerPoint(Converter):
|
|||||||
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
|
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
|
||||||
el_cnt = xpath(
|
el_cnt = xpath(
|
||||||
media.element.getparent().getparent().getparent(),
|
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]
|
)[0]
|
||||||
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
|
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
|
||||||
cond.set("delay", "0")
|
cond.set("delay", "0")
|
||||||
@ -556,22 +821,39 @@ class PowerPoint(Converter):
|
|||||||
|
|
||||||
|
|
||||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
"""Wrap a function to add a `--show-config` option."""
|
"""Wrap a function to add a '--show-config' option."""
|
||||||
|
|
||||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
def callback(ctx: Context, _param: Parameter, value: bool) -> None:
|
||||||
if not value or ctx.resilient_parsing:
|
if not value or ctx.resilient_parsing:
|
||||||
return
|
return
|
||||||
|
|
||||||
to = ctx.params.get("to", "html")
|
if "to" in ctx.params:
|
||||||
|
to = ctx.params["to"]
|
||||||
|
cls = Converter.from_string(to)
|
||||||
|
elif "dest" in ctx.params:
|
||||||
|
dest = Path(ctx.params["dest"])
|
||||||
|
fmt = dest.suffix[1:].lower()
|
||||||
|
try:
|
||||||
|
cls = Converter.from_string(fmt)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||||
|
)
|
||||||
|
cls = RevealJS
|
||||||
|
else:
|
||||||
|
cls = RevealJS
|
||||||
|
|
||||||
converter = Converter.from_string(to)
|
if doc := getattr(cls, "__doc__", ""):
|
||||||
|
click.echo(textwrap.dedent(doc))
|
||||||
|
|
||||||
for key, field in converter.model_fields.items():
|
for key, field in cls.model_fields.items():
|
||||||
if field.is_required():
|
if field.is_required():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
default = field.get_default(call_default_factory=True)
|
default = field.get_default(call_default_factory=True)
|
||||||
click.echo(f"{key}: {default}")
|
click.echo(click.style(key, bold=True) + f": {default}")
|
||||||
|
if description := field.description:
|
||||||
|
click.secho(textwrap.indent(description, prefix="# "), dim=True)
|
||||||
|
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
|
||||||
@ -587,18 +869,31 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
|
|
||||||
|
|
||||||
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
"""Wrap a function to add a `--show-template` option."""
|
"""Wrap a function to add a '--show-template' option."""
|
||||||
|
|
||||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
if not value or ctx.resilient_parsing:
|
if not value or ctx.resilient_parsing:
|
||||||
return
|
return
|
||||||
|
|
||||||
to = ctx.params.get("to", "html")
|
if "to" in ctx.params:
|
||||||
|
to = ctx.params["to"]
|
||||||
|
cls = Converter.from_string(to)
|
||||||
|
elif "dest" in ctx.params:
|
||||||
|
dest = Path(ctx.params["dest"])
|
||||||
|
fmt = dest.suffix[1:].lower()
|
||||||
|
try:
|
||||||
|
cls = Converter.from_string(fmt)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||||
|
)
|
||||||
|
cls = RevealJS
|
||||||
|
else:
|
||||||
|
cls = RevealJS
|
||||||
|
|
||||||
template = ctx.params.get("template", None)
|
template = ctx.params.get("template", None)
|
||||||
|
|
||||||
converter = Converter.from_string(to)(
|
converter = cls(presentation_configs=[], template=template)
|
||||||
presentation_configs=[PresentationConfig()], template=template
|
|
||||||
)
|
|
||||||
click.echo(converter.load_template())
|
click.echo(converter.load_template())
|
||||||
|
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
@ -620,7 +915,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
|
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
|
||||||
@click.option(
|
@click.option(
|
||||||
"--to",
|
"--to",
|
||||||
type=click.Choice(["auto", "html", "pdf", "pptx"], case_sensitive=False),
|
type=click.Choice(["auto", "html", "pdf", "pptx", "zip"], case_sensitive=False),
|
||||||
metavar="FORMAT",
|
metavar="FORMAT",
|
||||||
default="auto",
|
default="auto",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
@ -632,7 +927,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Open the newly created file using the appropriate application.",
|
help="Open the newly created file using the appropriate application.",
|
||||||
)
|
)
|
||||||
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
@ -640,7 +934,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
multiple=True,
|
multiple=True,
|
||||||
callback=validate_config_option,
|
callback=validate_config_option,
|
||||||
help="Configuration options passed to the converter. "
|
help="Configuration options passed to the converter. "
|
||||||
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
|
"E.g., pass '-cslide_number=true' to display slide numbers.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--use-template",
|
"--use-template",
|
||||||
@ -648,7 +942,19 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||||
help="Use the template given by FILE instead of default one. "
|
help="Use the template given by FILE instead of default one. "
|
||||||
"To echo the default template, use ``--show-template``.",
|
"To echo the default template, use '--show-template'.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--one-file",
|
||||||
|
is_flag=True,
|
||||||
|
help="Embed all local assets (e.g., video files) in the output file. "
|
||||||
|
"The is a convenient alias to '-cone_file=true'.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--offline",
|
||||||
|
is_flag=True,
|
||||||
|
help="Download any remote content and store it in the assets folder. "
|
||||||
|
"The is a convenient alias to '-coffline=true'.",
|
||||||
)
|
)
|
||||||
@show_template_option
|
@show_template_option
|
||||||
@show_config_options
|
@show_config_options
|
||||||
@ -659,9 +965,10 @@ def convert(
|
|||||||
dest: Path,
|
dest: Path,
|
||||||
to: str,
|
to: str,
|
||||||
open_result: bool,
|
open_result: bool,
|
||||||
force: bool,
|
|
||||||
config_options: dict[str, str],
|
config_options: dict[str, str],
|
||||||
template: Optional[Path],
|
template: Optional[Path],
|
||||||
|
offline: bool,
|
||||||
|
one_file: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||||
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||||
@ -672,13 +979,42 @@ def convert(
|
|||||||
try:
|
try:
|
||||||
cls = Converter.from_string(fmt)
|
cls = Converter.from_string(fmt)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||||
)
|
)
|
||||||
cls = RevealJS
|
cls = RevealJS
|
||||||
else:
|
else:
|
||||||
cls = Converter.from_string(to)
|
cls = Converter.from_string(to)
|
||||||
|
|
||||||
|
if (
|
||||||
|
one_file
|
||||||
|
and issubclass(cls, (RevealJS, HtmlZip))
|
||||||
|
and "one_file" not in config_options
|
||||||
|
):
|
||||||
|
config_options["one_file"] = "true"
|
||||||
|
|
||||||
|
# Change data_uri to one_file and print a warning if present
|
||||||
|
if "data_uri" in config_options:
|
||||||
|
warnings.warn(
|
||||||
|
"The 'data_uri' configuration option is deprecated and will be "
|
||||||
|
"removed in the next major version. "
|
||||||
|
"Use 'one_file' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
config_options["one_file"] = (
|
||||||
|
config_options["one_file"]
|
||||||
|
if "one_file" in config_options
|
||||||
|
else config_options.pop("data_uri")
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
offline
|
||||||
|
and issubclass(cls, (RevealJS, HtmlZip))
|
||||||
|
and "offline" not in config_options
|
||||||
|
):
|
||||||
|
config_options["offline"] = "true"
|
||||||
|
|
||||||
converter = cls(
|
converter = cls(
|
||||||
presentation_configs=presentation_configs,
|
presentation_configs=presentation_configs,
|
||||||
template=template,
|
template=template,
|
||||||
|
@ -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
|
you should probably not include examples that rely on external files, since
|
||||||
relative paths risk to be broken.
|
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
|
Options
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@ -181,6 +194,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import csv
|
import csv
|
||||||
import itertools as it
|
import itertools as it
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
@ -269,9 +283,9 @@ class ManimSlidesDirective(Directive):
|
|||||||
# Rendering is skipped if the tag skip-manim is present,
|
# Rendering is skipped if the tag skip-manim is present,
|
||||||
# or if we are making the pot-files
|
# or if we are making the pot-files
|
||||||
should_skip = (
|
should_skip = (
|
||||||
"skip-manim-slides"
|
self.state.document.settings.env.app.builder.tags.has("skip-manim-slides")
|
||||||
in self.state.document.settings.env.app.builder.tags.tags
|
|
||||||
or self.state.document.settings.env.app.builder.name == "gettext"
|
or self.state.document.settings.env.app.builder.name == "gettext"
|
||||||
|
or "SKIP_MANIM_SLIDES" in os.environ
|
||||||
)
|
)
|
||||||
if should_skip:
|
if should_skip:
|
||||||
node = SkipManimNode()
|
node = SkipManimNode()
|
||||||
@ -325,7 +339,7 @@ class ManimSlidesDirective(Directive):
|
|||||||
ref_block = ""
|
ref_block = ""
|
||||||
|
|
||||||
if "quality" in self.options:
|
if "quality" in self.options:
|
||||||
quality = f'{self.options["quality"]}_quality'
|
quality = f"{self.options['quality']}_quality"
|
||||||
else:
|
else:
|
||||||
quality = "example_quality"
|
quality = "example_quality"
|
||||||
frame_rate = QUALITIES[quality]["frame_rate"]
|
frame_rate = QUALITIES[quality]["frame_rate"]
|
||||||
@ -441,7 +455,7 @@ def _write_rendering_stats(scene_name, run_time, file_name):
|
|||||||
[
|
[
|
||||||
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
|
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
|
||||||
scene_name,
|
scene_name,
|
||||||
"%.3f" % run_time,
|
f"{run_time:.3f}",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -469,7 +483,7 @@ def _log_rendering_times(*args):
|
|||||||
)
|
)
|
||||||
for row in group:
|
for row in group:
|
||||||
print( # noqa: T201
|
print( # noqa: T201
|
||||||
f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}"
|
f"{' ' * (max_file_length)} {row[2].rjust(7)}s {row[1]}"
|
||||||
)
|
)
|
||||||
print("") # noqa: T201
|
print("") # noqa: T201
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class ManimSlidesMagic(Magics): # type: ignore
|
|||||||
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
|
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
|
||||||
could look as follows::
|
could look as follows::
|
||||||
|
|
||||||
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
|
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true one_file=true
|
||||||
|
|
||||||
class MySlide(Slide):
|
class MySlide(Slide):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
@ -222,17 +222,29 @@ class ManimSlidesMagic(Magics): # type: ignore
|
|||||||
|
|
||||||
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
|
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
|
||||||
|
|
||||||
if embed: # Embedding implies data-uri
|
# If data_uri is set, raise a warning
|
||||||
kwargs["data_uri"] = "true"
|
if "data_uri" in kwargs:
|
||||||
|
logger.warning(
|
||||||
|
"'data_uri' configuration option is deprecated and will be removed in a future release. "
|
||||||
|
"Please use 'one_file' instead."
|
||||||
|
)
|
||||||
|
kwargs["one_file"] = (
|
||||||
|
kwargs["one_file"]
|
||||||
|
if "one_file" in kwargs
|
||||||
|
else kwargs.pop("data_uri")
|
||||||
|
)
|
||||||
|
|
||||||
|
if embed: # Embedding implies one_file
|
||||||
|
kwargs["one_file"] = "true"
|
||||||
|
|
||||||
# TODO: FIXME
|
# TODO: FIXME
|
||||||
# Seems like files are blocked so date-uri is the only working option...
|
# Seems like files are blocked so one_file is the only working option...
|
||||||
if kwargs.get("data_uri", "false").lower().strip() == "false":
|
if kwargs.get("one_file", "false").lower().strip() == "false":
|
||||||
logger.warn(
|
logger.warning(
|
||||||
"data_uri option is currently automatically enabled, "
|
"one_file option is currently automatically enabled, "
|
||||||
"because using local video files does not seem to work properly."
|
"because using local video files does not seem to work properly."
|
||||||
)
|
)
|
||||||
kwargs["data_uri"] = "true"
|
kwargs["one_file"] = "true"
|
||||||
|
|
||||||
presentation_configs = get_scenes_presentation_config(
|
presentation_configs = get_scenes_presentation_config(
|
||||||
[clsname], Path("./slides")
|
[clsname], Path("./slides")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple
|
from typing import Literal, Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from click import Context, Parameter
|
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 ..config import Config, PresentationConfig
|
||||||
from ..logger import logger
|
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()
|
@click.command()
|
||||||
@folder_path_option
|
@folder_path_option
|
||||||
@ -50,7 +33,7 @@ def _list_scenes(folder: Path) -> list[str]:
|
|||||||
except (
|
except (
|
||||||
Exception
|
Exception
|
||||||
) as e: # Could not parse this file as a proper presentation config
|
) 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}"
|
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -239,8 +222,14 @@ def start_at_callback(
|
|||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--hide-info-window",
|
"--hide-info-window",
|
||||||
is_flag=True,
|
flag_value="always",
|
||||||
help="Hide info window.",
|
help="Hide info window. By default, hide the info window if there is only one screen.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--show-info-window",
|
||||||
|
"hide_info_window",
|
||||||
|
flag_value="never",
|
||||||
|
help="Force to show info window.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--info-window-screen",
|
"--info-window-screen",
|
||||||
@ -248,11 +237,13 @@ def start_at_callback(
|
|||||||
metavar="NUMBER",
|
metavar="NUMBER",
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="Put info window on the given screen (a.k.a. display).",
|
help="Put info window on the given screen (a.k.a. display). "
|
||||||
|
"If there is more than one screen, it will by default put the info window "
|
||||||
|
"on a different screen than the main player.",
|
||||||
)
|
)
|
||||||
@click.help_option("-h", "--help")
|
@click.help_option("-h", "--help")
|
||||||
@verbosity_option
|
@verbosity_option
|
||||||
def present(
|
def present( # noqa: C901
|
||||||
scenes: list[str],
|
scenes: list[str],
|
||||||
config_path: Path,
|
config_path: Path,
|
||||||
folder: Path,
|
folder: Path,
|
||||||
@ -268,7 +259,7 @@ def present(
|
|||||||
screen_number: Optional[int],
|
screen_number: Optional[int],
|
||||||
playback_rate: float,
|
playback_rate: float,
|
||||||
next_terminates_loop: bool,
|
next_terminates_loop: bool,
|
||||||
hide_info_window: bool,
|
hide_info_window: Optional[Literal["always", "never"]],
|
||||||
info_window_screen_number: Optional[int],
|
info_window_screen_number: Optional[int],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -302,8 +293,6 @@ def present(
|
|||||||
if start_at[1]:
|
if start_at[1]:
|
||||||
start_at_slide_number = start_at[1]
|
start_at_slide_number = start_at[1]
|
||||||
|
|
||||||
warn_if_non_desirable_pyside6_version()
|
|
||||||
|
|
||||||
from qtpy.QtCore import Qt
|
from qtpy.QtCore import Qt
|
||||||
from qtpy.QtGui import QScreen
|
from qtpy.QtGui import QScreen
|
||||||
|
|
||||||
@ -313,22 +302,36 @@ def present(
|
|||||||
app = qapp()
|
app = qapp()
|
||||||
app.setApplicationName("Manim Slides")
|
app.setApplicationName("Manim Slides")
|
||||||
|
|
||||||
|
screens = app.screens()
|
||||||
|
|
||||||
def get_screen(number: int) -> Optional[QScreen]:
|
def get_screen(number: int) -> Optional[QScreen]:
|
||||||
try:
|
try:
|
||||||
return app.screens()[number]
|
return screens[number]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Invalid screen number {number}, "
|
f"Invalid screen number {number}, "
|
||||||
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
|
f"allowed values are from 0 to {len(screens) - 1} (incl.)"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
should_hide_info_window = False
|
||||||
|
|
||||||
|
if hide_info_window is None:
|
||||||
|
should_hide_info_window = len(screens) == 1
|
||||||
|
elif hide_info_window == "always":
|
||||||
|
should_hide_info_window = True
|
||||||
|
|
||||||
|
if should_hide_info_window and info_window_screen_number is not None:
|
||||||
|
logger.warning(
|
||||||
|
f"Ignoring `--info-window-screen` because `--hide-info-window` is set to `{hide_info_window}`."
|
||||||
|
)
|
||||||
|
|
||||||
if screen_number is not None:
|
if screen_number is not None:
|
||||||
screen = get_screen(screen_number)
|
screen = get_screen(screen_number)
|
||||||
else:
|
else:
|
||||||
screen = None
|
screen = None
|
||||||
|
|
||||||
if info_window_screen_number is not None:
|
if info_window_screen_number is not None and not should_hide_info_window:
|
||||||
info_window_screen = get_screen(info_window_screen_number)
|
info_window_screen = get_screen(info_window_screen_number)
|
||||||
else:
|
else:
|
||||||
info_window_screen = None
|
info_window_screen = None
|
||||||
@ -352,11 +355,11 @@ def present(
|
|||||||
screen=screen,
|
screen=screen,
|
||||||
playback_rate=playback_rate,
|
playback_rate=playback_rate,
|
||||||
next_terminates_loop=next_terminates_loop,
|
next_terminates_loop=next_terminates_loop,
|
||||||
hide_info_window=hide_info_window,
|
hide_info_window=should_hide_info_window,
|
||||||
info_window_screen=info_window_screen,
|
info_window_screen=info_window_screen,
|
||||||
)
|
)
|
||||||
|
|
||||||
player.show()
|
player.show(screens)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
|
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
|
||||||
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
|
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
|
||||||
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer
|
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer, QVideoFrame
|
||||||
from qtpy.QtMultimediaWidgets import QVideoWidget
|
from qtpy.QtMultimediaWidgets import QVideoWidget
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
@ -28,7 +28,6 @@ class Info(QWidget): # type: ignore[misc]
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
full_screen: bool,
|
|
||||||
aspect_ratio_mode: Qt.AspectRatioMode,
|
aspect_ratio_mode: Qt.AspectRatioMode,
|
||||||
screen: Optional[QScreen],
|
screen: Optional[QScreen],
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -38,9 +37,6 @@ class Info(QWidget): # type: ignore[misc]
|
|||||||
self.setScreen(screen)
|
self.setScreen(screen)
|
||||||
self.move(screen.geometry().topLeft())
|
self.move(screen.geometry().topLeft())
|
||||||
|
|
||||||
if full_screen:
|
|
||||||
self.setWindowState(Qt.WindowFullScreen)
|
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
|
|
||||||
# Current slide view
|
# Current slide view
|
||||||
@ -226,6 +222,8 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
self.icon = QIcon(":/icon.png")
|
self.icon = QIcon(":/icon.png")
|
||||||
self.setWindowIcon(self.icon)
|
self.setWindowIcon(self.icon)
|
||||||
|
|
||||||
|
self.frame = QVideoFrame()
|
||||||
|
|
||||||
self.audio_output = QAudioOutput()
|
self.audio_output = QAudioOutput()
|
||||||
self.video_widget = QVideoWidget()
|
self.video_widget = QVideoWidget()
|
||||||
self.video_sink = self.video_widget.videoSink()
|
self.video_sink = self.video_widget.videoSink()
|
||||||
@ -241,15 +239,12 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
self.slide_changed.connect(self.slide_changed_callback)
|
self.slide_changed.connect(self.slide_changed_callback)
|
||||||
|
|
||||||
self.info = Info(
|
self.info = Info(
|
||||||
full_screen=full_screen,
|
|
||||||
aspect_ratio_mode=aspect_ratio_mode,
|
aspect_ratio_mode=aspect_ratio_mode,
|
||||||
screen=info_window_screen,
|
screen=info_window_screen,
|
||||||
)
|
)
|
||||||
self.info.close_event.connect(self.closeEvent)
|
self.info.close_event.connect(self.closeEvent)
|
||||||
self.info.key_press_event.connect(self.keyPressEvent)
|
self.info.key_press_event.connect(self.keyPressEvent)
|
||||||
self.video_sink.videoFrameChanged.connect(
|
self.video_sink.videoFrameChanged.connect(self.frame_changed)
|
||||||
lambda frame: self.info.video_sink.setVideoFrame(frame)
|
|
||||||
)
|
|
||||||
self.hide_info_window = hide_info_window
|
self.hide_info_window = hide_info_window
|
||||||
|
|
||||||
# Connecting key callbacks
|
# Connecting key callbacks
|
||||||
@ -319,7 +314,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
elif -self.presentations_count <= index < 0:
|
elif -self.presentations_count <= index < 0:
|
||||||
self.__current_presentation_index = index + self.presentations_count
|
self.__current_presentation_index = index + self.presentations_count
|
||||||
else:
|
else:
|
||||||
logger.warn(f"Could not set presentation index to {index}.")
|
logger.warning(f"Could not set presentation index to {index}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.presentation_changed.emit()
|
self.presentation_changed.emit()
|
||||||
@ -343,7 +338,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
elif -self.current_slides_count <= index < 0:
|
elif -self.current_slides_count <= index < 0:
|
||||||
self.__current_slide_index = index + self.current_slides_count
|
self.__current_slide_index = index + self.current_slides_count
|
||||||
else:
|
else:
|
||||||
logger.warn(f"Could not set slide index to {index}.")
|
logger.warning(f"Could not set slide index to {index}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.slide_changed.emit()
|
self.slide_changed.emit()
|
||||||
@ -468,13 +463,13 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
def presentation_changed_callback(self) -> None:
|
def presentation_changed_callback(self) -> None:
|
||||||
index = self.current_presentation_index
|
index = self.current_presentation_index
|
||||||
count = self.presentations_count
|
count = self.presentations_count
|
||||||
self.info.scene_label.setText(f"{index+1:4d}/{count:4<d}")
|
self.info.scene_label.setText(f"{index + 1:4d}/{count:4<d}")
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def slide_changed_callback(self) -> None:
|
def slide_changed_callback(self) -> None:
|
||||||
index = self.current_slide_index
|
index = self.current_slide_index
|
||||||
count = self.current_slides_count
|
count = self.current_slides_count
|
||||||
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
|
self.info.slide_label.setText(f"{index + 1:4d}/{count:4<d}")
|
||||||
self.info.slide_notes.setText(self.current_slide_config.notes)
|
self.info.slide_notes.setText(self.current_slide_config.notes)
|
||||||
self.preview_next_slide()
|
self.preview_next_slide()
|
||||||
|
|
||||||
@ -484,11 +479,28 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
self.info.next_media_player.setSource(url)
|
self.info.next_media_player.setSource(url)
|
||||||
self.info.next_media_player.play()
|
self.info.next_media_player.play()
|
||||||
|
|
||||||
def show(self) -> None:
|
def show(self, screens: list[QScreen]) -> None:
|
||||||
|
"""Screens is necessary to prevent the info window from being shown on the same screen as the main window (especially in full screen mode)."""
|
||||||
super().show()
|
super().show()
|
||||||
|
|
||||||
if not self.hide_info_window:
|
if not self.hide_info_window:
|
||||||
self.info.show()
|
if len(screens) > 1 and self.isFullScreen():
|
||||||
|
self.ensure_different_screens(screens)
|
||||||
|
|
||||||
|
if self.isFullScreen():
|
||||||
|
self.info.showFullScreen()
|
||||||
|
else:
|
||||||
|
self.info.show()
|
||||||
|
|
||||||
|
if (
|
||||||
|
len(screens) > 1 and self.info.screen() == self.screen()
|
||||||
|
): # It is better when Qt assigns the location, but if it fails to, this is a fallback
|
||||||
|
self.ensure_different_screens(screens)
|
||||||
|
|
||||||
|
def ensure_different_screens(self, screens: list[QScreen]) -> None:
|
||||||
|
target_screen = screens[1] if self.screen() == screens[0] else screens[0]
|
||||||
|
self.info.setScreen(target_screen)
|
||||||
|
self.info.move(target_screen.geometry().topLeft())
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@ -515,6 +527,9 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def reverse(self) -> None:
|
def reverse(self) -> None:
|
||||||
|
if self.playing_reversed_slide and self.current_slide_index >= 1:
|
||||||
|
self.current_slide_index -= 1
|
||||||
|
|
||||||
self.load_reversed_slide()
|
self.load_reversed_slide()
|
||||||
self.preview_next_slide()
|
self.preview_next_slide()
|
||||||
|
|
||||||
@ -535,8 +550,10 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
def full_screen(self) -> None:
|
def full_screen(self) -> None:
|
||||||
if self.windowState() == Qt.WindowFullScreen:
|
if self.windowState() == Qt.WindowFullScreen:
|
||||||
self.setWindowState(Qt.WindowNoState)
|
self.setWindowState(Qt.WindowNoState)
|
||||||
|
self.info.setWindowState(Qt.WindowNoState)
|
||||||
else:
|
else:
|
||||||
self.setWindowState(Qt.WindowFullScreen)
|
self.setWindowState(Qt.WindowFullScreen)
|
||||||
|
self.info.setWindowState(Qt.WindowFullScreen)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def hide_mouse(self) -> None:
|
def hide_mouse(self) -> None:
|
||||||
@ -545,6 +562,34 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
else:
|
else:
|
||||||
self.setCursor(Qt.BlankCursor)
|
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
|
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Alias command to either
|
Alias command to either
|
||||||
``manim render [OPTIONS] [ARGS]...`` or
|
``manim render [OPTIONS] [ARGS]...`` or
|
||||||
``manimgl [OPTIONS] [ARGS]...``.
|
``manimgl -w [OPTIONS] [ARGS]...``.
|
||||||
|
|
||||||
This is especially useful for two reasons:
|
This is especially useful for two reasons:
|
||||||
|
|
||||||
@ -48,6 +48,7 @@ def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
|
|||||||
if ce and gl:
|
if ce and gl:
|
||||||
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
||||||
if gl:
|
if gl:
|
||||||
subprocess.run([sys.executable, "-m", "manimlib", *args])
|
completed = subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
|
||||||
else:
|
else:
|
||||||
subprocess.run([sys.executable, "-m", "manim", "render", *args])
|
completed = subprocess.run([sys.executable, "-m", "manim", "render", *args])
|
||||||
|
sys.exit(completed.returncode)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
|
"API_NAME",
|
||||||
"MANIM",
|
"MANIM",
|
||||||
"MANIMGL",
|
"MANIMGL",
|
||||||
"API_NAME",
|
|
||||||
"Slide",
|
"Slide",
|
||||||
"ThreeDSlide",
|
"ThreeDSlide",
|
||||||
]
|
]
|
||||||
@ -35,7 +35,7 @@ API: str = os.environ.get(MANIM_API, "manim").lower()
|
|||||||
|
|
||||||
if API not in API_NAMES:
|
if API not in API_NAMES:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
f"Specified MANIM_API={API!r} is not in valid options: " f"{API_NAMES}",
|
f"Specified MANIM_API={API!r} is not in valid options: {API_NAMES}",
|
||||||
)
|
)
|
||||||
|
|
||||||
API_NAME = API_NAMES[API]
|
API_NAME = API_NAMES[API]
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
__all__ = ["BaseSlide"]
|
__all__ = ["BaseSlide"]
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
import shutil
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import MutableMapping, Sequence, ValuesView
|
from collections.abc import MutableMapping, Sequence, ValuesView
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -32,6 +33,12 @@ LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
|
|||||||
|
|
||||||
|
|
||||||
class BaseSlide:
|
class BaseSlide:
|
||||||
|
disable_caching: bool = False
|
||||||
|
flush_cache: bool = False
|
||||||
|
skip_reversing: bool = False
|
||||||
|
max_duration_before_split_reverse: float | None = 4.0
|
||||||
|
num_processes: int | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
|
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -44,6 +51,7 @@ class BaseSlide:
|
|||||||
self._start_animation = 0
|
self._start_animation = 0
|
||||||
self._canvas: MutableMapping[str, Mobject] = {}
|
self._canvas: MutableMapping[str, Mobject] = {}
|
||||||
self._wait_time_between_slides = 0.0
|
self._wait_time_between_slides = 0.0
|
||||||
|
self._skip_animations = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -170,11 +178,23 @@ class BaseSlide:
|
|||||||
animations. You must still call :code:`self.add` or
|
animations. You must still call :code:`self.add` or
|
||||||
play some animation that introduces each Mobject for
|
play some animation that introduces each Mobject for
|
||||||
it to appear. The same applies when removing objects.
|
it to appear. The same applies when removing objects.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:attr:`canvas` for usage examples.
|
||||||
"""
|
"""
|
||||||
self._canvas.update(objects)
|
self._canvas.update(objects)
|
||||||
|
|
||||||
def remove_from_canvas(self, *names: str) -> None:
|
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:
|
for name in names:
|
||||||
self._canvas.pop(name)
|
self._canvas.pop(name)
|
||||||
|
|
||||||
@ -186,8 +206,12 @@ class BaseSlide:
|
|||||||
@property
|
@property
|
||||||
def mobjects_without_canvas(self) -> Sequence[Mobject]:
|
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.
|
the canvas.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:attr:`canvas` for usage examples.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
mobject
|
mobject
|
||||||
@ -256,7 +280,7 @@ class BaseSlide:
|
|||||||
self._wait_time_between_slides = max(wait_time, 0.0)
|
self._wait_time_between_slides = max(wait_time, 0.0)
|
||||||
|
|
||||||
def play(self, *args: Any, **kwargs: Any) -> None:
|
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Overload `self.play` and increment animation count."""
|
"""Overload 'self.play' and increment animation count."""
|
||||||
super().play(*args, **kwargs) # type: ignore[misc]
|
super().play(*args, **kwargs) # type: ignore[misc]
|
||||||
self._current_animation += 1
|
self._current_animation += 1
|
||||||
|
|
||||||
@ -275,36 +299,62 @@ class BaseSlide:
|
|||||||
next slide is played. By default, this is the right arrow key.
|
next slide is played. By default, this is the right arrow key.
|
||||||
|
|
||||||
:param args:
|
:param args:
|
||||||
Positional arguments to be passed to
|
Positional arguments passed to
|
||||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||||
or ignored if `manimlib` API is used.
|
or ignored if `manimlib` API is used.
|
||||||
|
:param skip_animations:
|
||||||
|
Exclude the next slide from the output.
|
||||||
|
|
||||||
|
If `manim` is used, this is also passed to :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||||
|
which will avoid rendering the corresponding animations.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:meth:`start_skip_animations`
|
||||||
|
:meth:`stop_skip_animations`
|
||||||
:param loop:
|
:param loop:
|
||||||
If set, next slide will be looping.
|
If set, next slide will be looping.
|
||||||
:param auto_next:
|
:param auto_next:
|
||||||
If set, next slide will play immediately play the next slide
|
If set, next slide will play immediately play the next slide
|
||||||
upon terminating.
|
upon terminating.
|
||||||
|
|
||||||
Note that this is only supported by ``manim-slides present``
|
.. warning::
|
||||||
and ``manim-slides convert --to=html``.
|
|
||||||
|
Only supported by ``manim-slides present``
|
||||||
|
and ``manim-slides convert --to=html``.
|
||||||
:param playback_rate:
|
:param playback_rate:
|
||||||
Playback rate at which the video is played.
|
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:
|
:param reversed_playback_rate:
|
||||||
Playback rate at which the reversed video is played.
|
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:
|
:param notes:
|
||||||
Presenter notes, in Markdown format.
|
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``
|
.. warning::
|
||||||
and ``manim-slides convert --to=html/pptx``.
|
|
||||||
|
Only supported by ``manim-slides present``,
|
||||||
|
``manim-slides convert --to=html`` and
|
||||||
|
``manim-slides convert --to=pptx``.
|
||||||
:param dedent_notes:
|
:param dedent_notes:
|
||||||
If set, apply :func:`textwrap.dedent` to notes.
|
If set, apply :func:`textwrap.dedent` to notes.
|
||||||
|
:param pathlib.Path src:
|
||||||
|
An optional path to a video file to include as next slide.
|
||||||
|
|
||||||
|
The video will be copied into the output folder, but no rescaling
|
||||||
|
is applied.
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
Keyword arguments to be passed to
|
Keyword arguments passed to
|
||||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||||
or ignored if `manimlib` API is used.
|
or ignored if `manimlib` API is used.
|
||||||
|
|
||||||
@ -426,6 +476,21 @@ class BaseSlide:
|
|||||||
|
|
||||||
self._current_slide += 1
|
self._current_slide += 1
|
||||||
|
|
||||||
|
if base_slide_config.src is not None:
|
||||||
|
self._slides.append(
|
||||||
|
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||||
|
base_slide_config,
|
||||||
|
self._current_animation,
|
||||||
|
self._current_animation,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
base_slide_config = BaseSlideConfig() # default
|
||||||
|
self._current_slide += 1
|
||||||
|
|
||||||
|
if self._skip_animations:
|
||||||
|
base_slide_config.skip_animations = True
|
||||||
|
|
||||||
self._base_slide_config = base_slide_config
|
self._base_slide_config = base_slide_config
|
||||||
self._start_animation = self._current_animation
|
self._start_animation = self._current_animation
|
||||||
|
|
||||||
@ -445,11 +510,17 @@ class BaseSlide:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _save_slides(self, use_cache: bool = True) -> None:
|
def _save_slides( # noqa: C901
|
||||||
|
self,
|
||||||
|
use_cache: bool = True,
|
||||||
|
flush_cache: bool = False,
|
||||||
|
skip_reversing: bool = False,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Save slides, optionally using cached files.
|
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()
|
self._add_last_slide()
|
||||||
|
|
||||||
@ -458,6 +529,9 @@ class BaseSlide:
|
|||||||
scene_name = str(self)
|
scene_name = str(self)
|
||||||
scene_files_folder = files_folder / scene_name
|
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)
|
scene_files_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
files: list[Path] = self._partial_movie_files
|
files: list[Path] = self._partial_movie_files
|
||||||
@ -475,14 +549,25 @@ class BaseSlide:
|
|||||||
|
|
||||||
for pre_slide_config in tqdm(
|
for pre_slide_config in tqdm(
|
||||||
self._slides,
|
self._slides,
|
||||||
desc=f"Concatenating animation files to '{scene_files_folder}' and generating reversed animations",
|
desc=f"Concatenating animations to '{scene_files_folder}' and generating reversed animations",
|
||||||
leave=self._leave_progress_bar,
|
leave=self._leave_progress_bar,
|
||||||
ascii=True if platform.system() == "Windows" else None,
|
ascii=True if platform.system() == "Windows" else None,
|
||||||
disable=not self._show_progress_bar,
|
disable=not self._show_progress_bar,
|
||||||
|
unit=" slides",
|
||||||
):
|
):
|
||||||
slide_files = files[pre_slide_config.slides_slice]
|
if pre_slide_config.skip_animations:
|
||||||
|
continue
|
||||||
|
if pre_slide_config.src:
|
||||||
|
slide_files = [pre_slide_config.src]
|
||||||
|
else:
|
||||||
|
slide_files = files[pre_slide_config.slides_slice]
|
||||||
|
|
||||||
file = merge_basenames(slide_files)
|
try:
|
||||||
|
file = merge_basenames(slide_files)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Failed to merge basenames of files for slide: {pre_slide_config!r}"
|
||||||
|
) from e
|
||||||
dst_file = scene_files_folder / file.name
|
dst_file = scene_files_folder / file.name
|
||||||
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
|
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
|
||||||
|
|
||||||
@ -492,7 +577,18 @@ class BaseSlide:
|
|||||||
|
|
||||||
# We only reverse video if it was not present
|
# We only reverse video if it was not present
|
||||||
if not use_cache or not rev_file.exists():
|
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,
|
||||||
|
max_segment_duration=self.max_duration_before_split_reverse,
|
||||||
|
num_processes=self.num_processes,
|
||||||
|
leave=self._leave_progress_bar,
|
||||||
|
ascii=True if platform.system() == "Windows" else None,
|
||||||
|
disable=not self._show_progress_bar,
|
||||||
|
)
|
||||||
|
|
||||||
slides.append(
|
slides.append(
|
||||||
SlideConfig.from_pre_slide_config_and_files(
|
SlideConfig.from_pre_slide_config_and_files(
|
||||||
@ -516,6 +612,22 @@ class BaseSlide:
|
|||||||
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
|
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def start_skip_animations(self) -> None:
|
||||||
|
"""
|
||||||
|
Start skipping animations.
|
||||||
|
|
||||||
|
This automatically applies ``skip_animations=True``
|
||||||
|
to all subsequent calls to :meth:`next_slide`.
|
||||||
|
|
||||||
|
This is useful when you want to skip animations from multiple slides in a row,
|
||||||
|
without having to manually set ``skip_animations=True``.
|
||||||
|
"""
|
||||||
|
self._skip_animations = True
|
||||||
|
|
||||||
|
def stop_skip_animations(self) -> None:
|
||||||
|
"""Stop skipping animations."""
|
||||||
|
self._skip_animations = False
|
||||||
|
|
||||||
def wipe(
|
def wipe(
|
||||||
self,
|
self,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
|
@ -2,6 +2,8 @@ from pathlib import Path
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from manim import Scene, ThreeDScene, config
|
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 ..config import BaseSlideConfig
|
||||||
from .base import BaseSlide
|
from .base import BaseSlide
|
||||||
@ -9,29 +11,77 @@ from .base import BaseSlide
|
|||||||
|
|
||||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||||
"""
|
"""
|
||||||
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
|
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provides necessary tools
|
||||||
for slides rendering.
|
for slides rendering.
|
||||||
|
|
||||||
|
:param args: Positional arguments passed to scene object.
|
||||||
|
:param pathlib.Path output_folder: Where the slide animation files should be written.
|
||||||
|
:param kwargs: Keyword arguments passed to scene object.
|
||||||
|
:cvar bool disable_caching: :data:`False`: Whether to disable the use of
|
||||||
|
cached animation files.
|
||||||
|
:cvar bool flush_cache: :data:`False`: Whether to flush the cache.
|
||||||
|
Unlike with Manim, flushing is performed before rendering.
|
||||||
|
:cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations.
|
||||||
|
If set to :data:`False`, and no cached reversed animation
|
||||||
|
exists (or caching is disabled) for a given slide,
|
||||||
|
then the reversed animation will be simply the same
|
||||||
|
as the original one, i.e., ``rev_file = file``,
|
||||||
|
for the current slide config.
|
||||||
|
:cvar typing.Optional[float] max_duration_before_split_reverse: :data:`4.0`: Maximum duration
|
||||||
|
before of a video animation before it is reversed by splitting the file into smaller chunks.
|
||||||
|
Generating reversed animations can require an important amount of
|
||||||
|
memory (because the whole video needs to be kept in memory),
|
||||||
|
and splitting the video into multiple chunks usually speeds
|
||||||
|
up the process (because it can be done in parallel) while taking
|
||||||
|
less memory.
|
||||||
|
Set this to :data:`None` to disable splitting the file into chunks.
|
||||||
|
:cvar typing.Optional[int] num_processes: :data:`None`: Number of processes
|
||||||
|
to use for parallelizable operations.
|
||||||
|
If :data:`None`, defaults to :func:`os.process_cpu_count`.
|
||||||
|
This is currently used when generating reversed animations, and can
|
||||||
|
increase memory consumption.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
# OpenGL renderer disables 'write_to_movie' by default
|
||||||
|
# which is required for saving the animations
|
||||||
|
config["write_to_movie"] = True
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _frame_shape(self) -> tuple[float, float]:
|
||||||
|
if isinstance(self.renderer, OpenGLRenderer):
|
||||||
|
return self.renderer.camera.frame_shape # type: ignore
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
self.renderer.camera.frame_height,
|
||||||
|
self.renderer.camera.frame_width,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _frame_height(self) -> float:
|
def _frame_height(self) -> float:
|
||||||
return config["frame_height"] # type: ignore
|
return self._frame_shape[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _frame_width(self) -> float:
|
def _frame_width(self) -> float:
|
||||||
return config["frame_width"] # type: ignore
|
return self._frame_shape[1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _background_color(self) -> str:
|
def _background_color(self) -> str:
|
||||||
color = self.camera.background_color
|
if isinstance(self.renderer, OpenGLRenderer):
|
||||||
if hex_color := getattr(color, "hex", None):
|
return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore
|
||||||
return hex_color # type: ignore
|
else:
|
||||||
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
|
return self.renderer.camera.background_color.to_hex() # type: ignore
|
||||||
return color.to_hex() # type: ignore
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _resolution(self) -> tuple[int, int]:
|
def _resolution(self) -> tuple[int, int]:
|
||||||
return config["pixel_width"], config["pixel_height"]
|
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
|
@property
|
||||||
def _partial_movie_files(self) -> list[Path]:
|
def _partial_movie_files(self) -> list[Path]:
|
||||||
@ -56,6 +106,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
|||||||
def _start_at_animation_number(self) -> Optional[int]:
|
def _start_at_animation_number(self) -> Optional[int]:
|
||||||
return config["from_animation_number"] # type: ignore
|
return config["from_animation_number"] # type: ignore
|
||||||
|
|
||||||
|
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Overload 'self.play' and increment animation count."""
|
||||||
|
super().play(*args, **kwargs)
|
||||||
|
|
||||||
|
if self._base_slide_config.skip_animations:
|
||||||
|
# Manim will not render the animations, so we reset the animation
|
||||||
|
# counter to the previous value
|
||||||
|
self._current_animation -= 1
|
||||||
|
|
||||||
def next_section(self, *args: Any, **kwargs: Any) -> None:
|
def next_section(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Alias to :meth:`next_slide`.
|
Alias to :meth:`next_slide`.
|
||||||
@ -78,23 +137,41 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
|||||||
base_slide_config: BaseSlideConfig,
|
base_slide_config: BaseSlideConfig,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
Scene.next_section(self, *args, **kwargs)
|
Scene.next_section(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
skip_animations=base_slide_config.skip_animations | self._skip_animations,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
BaseSlide.next_slide.__wrapped__(
|
BaseSlide.next_slide.__wrapped__(
|
||||||
self,
|
self,
|
||||||
base_slide_config=base_slide_config,
|
base_slide_config=base_slide_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
def render(self, *args: Any, **kwargs: Any) -> None:
|
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
|
# We need to disable the caching limit since we rely on intermediate files
|
||||||
max_files_cached = config["max_files_cached"]
|
max_files_cached = config["max_files_cached"]
|
||||||
config["max_files_cached"] = float("inf")
|
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)
|
super().render(*args, **kwargs)
|
||||||
|
|
||||||
config["max_files_cached"] = max_files_cached
|
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]
|
class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
|
||||||
|
@ -2,7 +2,6 @@ from pathlib import Path
|
|||||||
from typing import Any, ClassVar, Optional
|
from typing import Any, ClassVar, Optional
|
||||||
|
|
||||||
from manimlib import Scene, ThreeDCamera
|
from manimlib import Scene, ThreeDCamera
|
||||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
|
||||||
|
|
||||||
from .base import BaseSlide
|
from .base import BaseSlide
|
||||||
|
|
||||||
@ -10,42 +9,40 @@ from .base import BaseSlide
|
|||||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
kwargs.setdefault("file_writer_config", {}).update(
|
kwargs.setdefault("file_writer_config", {}).update(
|
||||||
skip_animations=True,
|
subdivide_output=True,
|
||||||
break_into_partial_movies=True,
|
|
||||||
write_to_movie=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
kwargs["preview"] = False # Avoid opening a preview window
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _frame_height(self) -> float:
|
def _frame_height(self) -> float:
|
||||||
return self.camera.frame.get_height() # type: ignore
|
return float(self.camera.get_frame_height())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _frame_width(self) -> float:
|
def _frame_width(self) -> float:
|
||||||
return self.camera.frame.get_width() # type: ignore
|
return float(self.camera.get_frame_width())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _background_color(self) -> str:
|
def _background_color(self) -> str:
|
||||||
return self.camera_config["background_color"].hex # type: ignore
|
rgba = self.camera.background_rgba
|
||||||
|
r = int(255 * rgba[0])
|
||||||
|
g = int(255 * rgba[1])
|
||||||
|
b = int(255 * rgba[2])
|
||||||
|
if rgba[3] == 1.0:
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
|
a = int(255 * rgba[3])
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _resolution(self) -> tuple[int, int]:
|
def _resolution(self) -> tuple[int, int]:
|
||||||
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
|
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _partial_movie_files(self) -> list[Path]:
|
def _partial_movie_files(self) -> list[Path]:
|
||||||
kwargs = {
|
partial_movie_directory = self.file_writer.partial_movie_directory
|
||||||
"remove_non_integer_files": True,
|
extension = self.file_writer.movie_file_extension
|
||||||
"extension": self.file_writer.movie_file_extension,
|
return sorted(partial_movie_directory.glob(f"*{extension}"))
|
||||||
}
|
|
||||||
return [
|
|
||||||
Path(file)
|
|
||||||
for file in get_sorted_integer_files(
|
|
||||||
self.file_writer.partial_movie_directory, **kwargs
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _show_progress_bar(self) -> bool:
|
def _show_progress_bar(self) -> bool:
|
||||||
@ -62,7 +59,11 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
|||||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""MANIMGL renderer."""
|
"""MANIMGL renderer."""
|
||||||
super().run(*args, **kwargs)
|
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):
|
class ThreeDSlide(Slide):
|
||||||
|
@ -12,89 +12,81 @@
|
|||||||
<!-- Theme used for syntax highlighting of code -->
|
<!-- Theme used for syntax highlighting of code -->
|
||||||
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
|
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
|
||||||
|
|
||||||
<!-- <link rel="stylesheet" href="index.css"> -->
|
<!-- <link rel="stylesheet" href="index.css"> -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="reveal">
|
<div class="reveal">
|
||||||
<div class="slides">
|
<div class="slides">
|
||||||
{%- for presentation_config in presentation_configs -%}
|
{% for presentation_config in presentation_configs -%}
|
||||||
{% set outer_loop = loop %}
|
{%- set outer_loop = loop %}
|
||||||
{%- for slide_config in presentation_config.slides -%}
|
{% for slide_config in presentation_config.slides %}
|
||||||
{%- if data_uri -%}
|
{% if one_file %}
|
||||||
{% set file = file_to_data_uri(slide_config.file) %}
|
{% set file = file_to_data_uri(slide_config.file) %}
|
||||||
{%- else -%}
|
{% else %}
|
||||||
{% set file = assets_dir / slide_config.file.name %}
|
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
<section
|
<section
|
||||||
data-background-size={{ background_size }}
|
data-background-size={{ background_size }}
|
||||||
data-background-color="{{ presentation_config.background_color }}"
|
data-background-color="{{ presentation_config.background_color }}"
|
||||||
data-background-video="{{ file }}"
|
data-background-video="{{ file }}"
|
||||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
{% if loop.index == 1 and outer_loop.index == 1 %}
|
||||||
data-background-video-muted
|
data-background-video-muted
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
{% if slide_config.loop -%}
|
{% if slide_config.loop %}
|
||||||
data-background-video-loop
|
data-background-video-loop
|
||||||
{%- endif -%}
|
{% endif %}
|
||||||
{% if slide_config.auto_next -%}
|
{% if slide_config.auto_next %}
|
||||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||||
{%- endif -%}>
|
{% endif %}
|
||||||
{% if slide_config.notes != "" -%}
|
>
|
||||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
{% if slide_config.notes != "" %}
|
||||||
{%- endif %}
|
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||||
</section>
|
{% endif %}
|
||||||
{%- endfor -%}
|
</section>
|
||||||
{%- endfor -%}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
|
||||||
|
|
||||||
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
|
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
|
||||||
|
{% if has_notes %}
|
||||||
{% if has_notes -%}
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
|
{% endif %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
<!-- <script src="index.js"></script> -->
|
<!-- <script src="index.js"></script> -->
|
||||||
<script>
|
<script>
|
||||||
Reveal.initialize({
|
Reveal.initialize({
|
||||||
{% if has_notes -%}
|
{% if has_notes %}
|
||||||
plugins: [ RevealMarkdown, RevealNotes ],
|
/// The list of RevealJS plugins.
|
||||||
{%- endif %}
|
plugins: [ RevealMarkdown, RevealNotes ],
|
||||||
|
{% endif %}
|
||||||
// The "normal" size of the presentation, aspect ratio will
|
// The "normal" size of the presentation, aspect ratio will
|
||||||
// be preserved when the presentation is scaled to fit different
|
// be preserved when the presentation is scaled to fit different
|
||||||
// resolutions. Can be specified using percentage units.
|
// resolutions. Can be specified using percentage units.
|
||||||
width: {{ width }},
|
width: {{ width }},
|
||||||
height: {{ height }},
|
height: {{ height }},
|
||||||
|
|
||||||
// Factor of the display size that should remain empty around
|
// Factor of the display size that should remain empty around
|
||||||
// the content
|
// the content
|
||||||
margin: {{ margin }},
|
margin: {{ margin }},
|
||||||
|
|
||||||
// Bounds for smallest/largest possible scale to apply to content
|
// Bounds for smallest/largest possible scale to apply to content
|
||||||
minScale: {{ min_scale }},
|
minScale: {{ min_scale }},
|
||||||
maxScale: {{ max_scale }},
|
maxScale: {{ max_scale }},
|
||||||
|
|
||||||
// Display presentation control arrows
|
// Display presentation control arrows
|
||||||
controls: {{ controls }},
|
controls: {{ controls }},
|
||||||
|
|
||||||
// Help the user learn the controls by providing hints, for example by
|
// Help the user learn the controls by providing hints, for example by
|
||||||
// bouncing the down arrow when they first encounter a vertical slide
|
// bouncing the down arrow when they first encounter a vertical slide
|
||||||
controlsTutorial: {{ controls_tutorial }},
|
controlsTutorial: {{ controls_tutorial }},
|
||||||
|
|
||||||
// Determines where controls appear, "edges" or "bottom-right"
|
// Determines where controls appear, "edges" or "bottom-right"
|
||||||
controlsLayout: {{ controls_layout }},
|
controlsLayout: {{ controls_layout }},
|
||||||
|
|
||||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
||||||
// or "visible"
|
// or "visible"
|
||||||
controlsBackArrows: {{ controls_back_arrows }},
|
controlsBackArrows: {{ controls_back_arrows }},
|
||||||
|
|
||||||
// Display a presentation progress bar
|
// Display a presentation progress bar
|
||||||
progress: {{ progress }},
|
progress: {{ progress }},
|
||||||
|
|
||||||
// Display the page number of the current slide
|
// Display the page number of the current slide
|
||||||
// - true: Show slide number
|
// - true: Show slide number
|
||||||
// - false: Hide slide number
|
// - false: Hide slide number
|
||||||
@ -110,55 +102,43 @@
|
|||||||
// object and return an array with one string [slideNumber] or
|
// object and return an array with one string [slideNumber] or
|
||||||
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
||||||
slideNumber: {{ slide_number }},
|
slideNumber: {{ slide_number }},
|
||||||
|
|
||||||
// Can be used to limit the contexts in which the slide number appears
|
// Can be used to limit the contexts in which the slide number appears
|
||||||
// - "all": Always show the slide number
|
// - "all": Always show the slide number
|
||||||
// - "print": Only when printing to PDF
|
// - "print": Only when printing to PDF
|
||||||
// - "speaker": Only in the speaker view
|
// - "speaker": Only in the speaker view
|
||||||
showSlideNumber: {{ show_slide_number }},
|
showSlideNumber: {{ show_slide_number }},
|
||||||
|
|
||||||
// Use 1 based indexing for # links to match slide number (default is zero
|
// Use 1 based indexing for # links to match slide number (default is zero
|
||||||
// based)
|
// based)
|
||||||
hashOneBasedIndex: {{ hash_one_based_index }},
|
hashOneBasedIndex: {{ hash_one_based_index }},
|
||||||
|
|
||||||
// Add the current slide number to the URL hash so that reloading the
|
// Add the current slide number to the URL hash so that reloading the
|
||||||
// page/copying the URL will return you to the same slide
|
// page/copying the URL will return you to the same slide
|
||||||
hash: {{ hash }},
|
hash: {{ hash }},
|
||||||
|
|
||||||
// Flags if we should monitor the hash and change slides accordingly
|
// Flags if we should monitor the hash and change slides accordingly
|
||||||
respondToHashChanges: {{ respond_to_hash_changes }},
|
respondToHashChanges: {{ respond_to_hash_changes }},
|
||||||
|
// Enable support for jump-to-slide navigation shortcuts
|
||||||
|
jumpToSlide: {{ jump_to_slide }},
|
||||||
// Push each slide change to the browser history. Implies `hash: true`
|
// Push each slide change to the browser history. Implies `hash: true`
|
||||||
history: {{ history }},
|
history: {{ history }},
|
||||||
|
|
||||||
// Enable keyboard shortcuts for navigation
|
// Enable keyboard shortcuts for navigation
|
||||||
keyboard: {{ keyboard }},
|
keyboard: {{ keyboard }},
|
||||||
|
|
||||||
// Optional function that blocks keyboard events when retuning false
|
// Optional function that blocks keyboard events when retuning false
|
||||||
//
|
//
|
||||||
// If you set this to 'focused', we will only capture keyboard events
|
// If you set this to 'focused', we will only capture keyboard events
|
||||||
// for embedded decks when they are in focus
|
// for embedded decks when they are in focus
|
||||||
keyboardCondition: {{ keyboard_condition }},
|
keyboardCondition: {{ keyboard_condition }},
|
||||||
|
|
||||||
// Disables the default reveal.js slide layout (scaling and centering)
|
// Disables the default reveal.js slide layout (scaling and centering)
|
||||||
// so that you can use custom CSS layout
|
// so that you can use custom CSS layout
|
||||||
disableLayout: {{ disable_layout }},
|
disableLayout: {{ disable_layout }},
|
||||||
|
|
||||||
// Enable the slide overview mode
|
// Enable the slide overview mode
|
||||||
overview: {{ overview }},
|
overview: {{ overview }},
|
||||||
|
|
||||||
// Vertical centering of slides
|
// Vertical centering of slides
|
||||||
center: {{ center }},
|
center: {{ center }},
|
||||||
|
|
||||||
// Enables touch navigation on devices with touch input
|
// Enables touch navigation on devices with touch input
|
||||||
touch: {{ touch }},
|
touch: {{ touch }},
|
||||||
|
|
||||||
// Loop the presentation
|
// Loop the presentation
|
||||||
loop: {{ loop }},
|
loop: {{ loop }},
|
||||||
|
|
||||||
// Change the presentation direction to be RTL
|
// Change the presentation direction to be RTL
|
||||||
rtl: {{ rtl }},
|
rtl: {{ rtl }},
|
||||||
|
|
||||||
// Changes the behavior of our navigation directions.
|
// Changes the behavior of our navigation directions.
|
||||||
//
|
//
|
||||||
// "default"
|
// "default"
|
||||||
@ -184,168 +164,171 @@
|
|||||||
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
||||||
// from 1.3 -> 2.3.
|
// from 1.3 -> 2.3.
|
||||||
navigationMode: {{ navigation_mode }},
|
navigationMode: {{ navigation_mode }},
|
||||||
|
|
||||||
// Randomizes the order of slides each time the presentation loads
|
// Randomizes the order of slides each time the presentation loads
|
||||||
shuffle: {{ shuffle }},
|
shuffle: {{ shuffle }},
|
||||||
|
|
||||||
// Turns fragments on and off globally
|
// Turns fragments on and off globally
|
||||||
fragments: {{ fragments }},
|
fragments: {{ fragments }},
|
||||||
|
|
||||||
// Flags whether to include the current fragment in the URL,
|
// Flags whether to include the current fragment in the URL,
|
||||||
// so that reloading brings you to the same fragment position
|
// so that reloading brings you to the same fragment position
|
||||||
fragmentInURL: {{ fragment_in_url }},
|
fragmentInURL: {{ fragment_in_url }},
|
||||||
|
|
||||||
// Flags if the presentation is running in an embedded mode,
|
// Flags if the presentation is running in an embedded mode,
|
||||||
// i.e. contained within a limited portion of the screen
|
// i.e. contained within a limited portion of the screen
|
||||||
embedded: {{ embedded }},
|
embedded: {{ embedded }},
|
||||||
|
|
||||||
// Flags if we should show a help overlay when the question-mark
|
// Flags if we should show a help overlay when the question-mark
|
||||||
// key is pressed
|
// key is pressed
|
||||||
help: {{ help }},
|
help: {{ help }},
|
||||||
|
|
||||||
// Flags if it should be possible to pause the presentation (blackout)
|
// Flags if it should be possible to pause the presentation (blackout)
|
||||||
pause: {{ pause }},
|
pause: {{ pause }},
|
||||||
|
|
||||||
// Flags if speaker notes should be visible to all viewers
|
// Flags if speaker notes should be visible to all viewers
|
||||||
showNotes: {{ show_notes }},
|
showNotes: {{ show_notes }},
|
||||||
|
|
||||||
// Global override for autolaying embedded media (video/audio/iframe)
|
// Global override for autolaying embedded media (video/audio/iframe)
|
||||||
// - null: Media will only autoplay if data-autoplay is present
|
// - null: Media will only autoplay if data-autoplay is present
|
||||||
// - true: All media will autoplay, regardless of individual setting
|
// - true: All media will autoplay, regardless of individual setting
|
||||||
// - false: No media will autoplay, regardless of individual setting
|
// - false: No media will autoplay, regardless of individual setting
|
||||||
autoPlayMedia: {{ auto_play_media }},
|
autoPlayMedia: {{ auto_play_media }},
|
||||||
|
|
||||||
// Global override for preloading lazy-loaded iframes
|
// Global override for preloading lazy-loaded iframes
|
||||||
// - null: Iframes with data-src AND data-preload will be loaded when within
|
// - null: Iframes with data-src AND data-preload will be loaded when within
|
||||||
// the viewDistance, iframes with only data-src will be loaded when visible
|
// the viewDistance, iframes with only data-src will be loaded when visible
|
||||||
// - true: All iframes with data-src will be loaded when within the viewDistance
|
// - true: All iframes with data-src will be loaded when within the viewDistance
|
||||||
// - false: All iframes with data-src will be loaded only when visible
|
// - false: All iframes with data-src will be loaded only when visible
|
||||||
preloadIframes: {{ preload_iframes }},
|
preloadIframes: {{ preload_iframes }},
|
||||||
|
|
||||||
// Can be used to globally disable auto-animation
|
// Can be used to globally disable auto-animation
|
||||||
autoAnimate: {{ auto_animate }},
|
autoAnimate: {{ auto_animate }},
|
||||||
|
|
||||||
// Optionally provide a custom element matcher that will be
|
// Optionally provide a custom element matcher that will be
|
||||||
// used to dictate which elements we can animate between.
|
// used to dictate which elements we can animate between.
|
||||||
autoAnimateMatcher: {{ auto_animate_matcher }},
|
autoAnimateMatcher: {{ auto_animate_matcher }},
|
||||||
|
|
||||||
// Default settings for our auto-animate transitions, can be
|
// Default settings for our auto-animate transitions, can be
|
||||||
// overridden per-slide or per-element via data arguments
|
// overridden per-slide or per-element via data arguments
|
||||||
autoAnimateEasing: {{ auto_animate_easing }},
|
autoAnimateEasing: {{ auto_animate_easing }},
|
||||||
autoAnimateDuration: {{ auto_animate_duration }},
|
autoAnimateDuration: {{ auto_animate_duration }},
|
||||||
autoAnimateUnmatched: {{ auto_animate_unmatched }},
|
autoAnimateUnmatched: {{ auto_animate_unmatched }},
|
||||||
|
|
||||||
// CSS properties that can be auto-animated. Position & scale
|
// CSS properties that can be auto-animated. Position & scale
|
||||||
// is matched separately so there's no need to include styles
|
// is matched separately so there's no need to include styles
|
||||||
// like top/right/bottom/left, width/height or margin.
|
// like top/right/bottom/left, width/height or margin.
|
||||||
autoAnimateStyles: {{ auto_animate_styles }},
|
autoAnimateStyles: {{ auto_animate_styles }},
|
||||||
|
|
||||||
// Controls automatic progression to the next slide
|
// Controls automatic progression to the next slide
|
||||||
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
||||||
// is present on the current slide or fragment
|
// is present on the current slide or fragment
|
||||||
// - 1+: All slides will progress automatically at the given interval
|
// - 1+: All slides will progress automatically at the given interval
|
||||||
// - false: No auto-sliding, even if data-autoslide is present
|
// - false: No auto-sliding, even if data-autoslide is present
|
||||||
autoSlide: {{ auto_slide }},
|
autoSlide: {{ auto_slide }},
|
||||||
|
|
||||||
// Stop auto-sliding after user input
|
// Stop auto-sliding after user input
|
||||||
autoSlideStoppable: {{ auto_slide_stoppable }},
|
autoSlideStoppable: {{ auto_slide_stoppable }},
|
||||||
|
|
||||||
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
||||||
autoSlideMethod: {{ auto_slide_method }},
|
autoSlideMethod: {{ auto_slide_method }},
|
||||||
|
|
||||||
// Specify the average time in seconds that you think you will spend
|
// Specify the average time in seconds that you think you will spend
|
||||||
// presenting each slide. This is used to show a pacing timer in the
|
// presenting each slide. This is used to show a pacing timer in the
|
||||||
// speaker view
|
// speaker view
|
||||||
defaultTiming: {{ default_timing }},
|
defaultTiming: {{ default_timing }},
|
||||||
|
|
||||||
// Enable slide navigation via mouse wheel
|
// Enable slide navigation via mouse wheel
|
||||||
mouseWheel: {{ mouse_wheel }},
|
mouseWheel: {{ mouse_wheel }},
|
||||||
|
|
||||||
// Opens links in an iframe preview overlay
|
// Opens links in an iframe preview overlay
|
||||||
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
||||||
// individually
|
// individually
|
||||||
previewLinks: {{ preview_links }},
|
previewLinks: {{ preview_links }},
|
||||||
|
|
||||||
// Exposes the reveal.js API through window.postMessage
|
// Exposes the reveal.js API through window.postMessage
|
||||||
postMessage: {{ post_message }},
|
postMessage: {{ post_message }},
|
||||||
|
|
||||||
// Dispatches all reveal.js events to the parent window through postMessage
|
// Dispatches all reveal.js events to the parent window through postMessage
|
||||||
postMessageEvents: {{ post_message_events }},
|
postMessageEvents: {{ post_message_events }},
|
||||||
|
|
||||||
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
||||||
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
|
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
|
||||||
|
|
||||||
// Transition style
|
// Transition style
|
||||||
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
|
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
|
||||||
|
|
||||||
// Transition speed
|
// Transition speed
|
||||||
transitionSpeed: {{ transition_speed }}, // default/fast/slow
|
transitionSpeed: {{ transition_speed }}, // default/fast/slow
|
||||||
|
|
||||||
// Transition style for full page slide backgrounds
|
// Transition style for full page slide backgrounds
|
||||||
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
|
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
|
||||||
|
|
||||||
// The maximum number of pages a single slide can expand onto when printing
|
// The maximum number of pages a single slide can expand onto when printing
|
||||||
// to PDF, unlimited by default
|
// to PDF, unlimited by default
|
||||||
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
|
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
|
||||||
|
|
||||||
// Prints each fragment on a separate slide
|
// Prints each fragment on a separate slide
|
||||||
pdfSeparateFragments: {{ pdf_separate_fragments }},
|
pdfSeparateFragments: {{ pdf_separate_fragments }},
|
||||||
|
|
||||||
// Offset used to reduce the height of content within exported PDF pages.
|
// Offset used to reduce the height of content within exported PDF pages.
|
||||||
// This exists to account for environment differences based on how you
|
// This exists to account for environment differences based on how you
|
||||||
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
||||||
// on precisely the total height of the document whereas in-browser
|
// on precisely the total height of the document whereas in-browser
|
||||||
// printing has to end one pixel before.
|
// printing has to end one pixel before.
|
||||||
pdfPageHeightOffset: {{ pdf_page_height_offset }},
|
pdfPageHeightOffset: {{ pdf_page_height_offset }},
|
||||||
|
|
||||||
// Number of slides away from the current that are visible
|
// Number of slides away from the current that are visible
|
||||||
viewDistance: {{ view_distance }},
|
viewDistance: {{ view_distance }},
|
||||||
|
|
||||||
// Number of slides away from the current that are visible on mobile
|
// Number of slides away from the current that are visible on mobile
|
||||||
// devices. It is advisable to set this to a lower number than
|
// devices. It is advisable to set this to a lower number than
|
||||||
// viewDistance in order to save resources.
|
// viewDistance in order to save resources.
|
||||||
mobileViewDistance: {{ mobile_view_distance }},
|
mobileViewDistance: {{ mobile_view_distance }},
|
||||||
|
|
||||||
// The display mode that will be used to show slides
|
// The display mode that will be used to show slides
|
||||||
display: {{ display }},
|
display: {{ display }},
|
||||||
|
|
||||||
// Hide cursor if inactive
|
// Hide cursor if inactive
|
||||||
hideInactiveCursor: {{ hide_inactive_cursor }},
|
hideInactiveCursor: {{ hide_inactive_cursor }},
|
||||||
|
|
||||||
// Time before the cursor is hidden (in ms)
|
// Time before the cursor is hidden (in ms)
|
||||||
hideCursorTime: {{ hide_cursor_time }}
|
hideCursorTime: {{ hide_cursor_time }}
|
||||||
});
|
});
|
||||||
|
// Override SPACE to play / pause the video
|
||||||
|
Reveal.addKeyBinding(
|
||||||
|
{
|
||||||
|
keyCode: 32,
|
||||||
|
key: 'SPACE',
|
||||||
|
description: 'Play / pause video'
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
var currentVideos = Reveal.getCurrentSlide().slideBackgroundContentElement.getElementsByTagName("video");
|
||||||
|
if (currentVideos.length > 0) {
|
||||||
|
if (currentVideos[0].paused == true) currentVideos[0].play();
|
||||||
|
else currentVideos[0].pause();
|
||||||
|
} else {
|
||||||
|
Reveal.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
{% if one_file %}
|
||||||
|
// Fix found by @t-fritsch and @Rapsssito on GitHub
|
||||||
|
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
|
||||||
|
function setVideoBase64(video) {
|
||||||
|
const sources = video.querySelectorAll('source');
|
||||||
|
// Update the source of the video
|
||||||
|
sources.forEach((source, i) => {
|
||||||
|
const src = source.getAttribute('src');
|
||||||
|
if(src.match(/^data:video.*;base64$/)) {
|
||||||
|
const nextSrc = sources[i+1]?.getAttribute('src');
|
||||||
|
video.setAttribute('src', `${src},${nextSrc}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{% if data_uri -%}
|
|
||||||
// Fix found by @t-fritsch on GitHub
|
|
||||||
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
|
|
||||||
function fixBase64VideoBackground(event) {
|
function fixBase64VideoBackground(event) {
|
||||||
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
|
// Analyze all slides backgrounds
|
||||||
if (event.currentSlide.getAttribute('data-background-video')) {
|
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
|
||||||
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
|
// Get the slide video and its sources for each background
|
||||||
video = background.querySelector('video'),
|
const video = slide.querySelector('video');
|
||||||
sources = video.querySelectorAll('source');
|
if (video) {
|
||||||
|
setVideoBase64(video);
|
||||||
sources.forEach((source, i) => {
|
} else {
|
||||||
const src = source.getAttribute('src');
|
// Listen to the creation of the video element
|
||||||
if(src.match(/^data:video.*;base64$/)) {
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
const nextSrc = sources[i+1]?.getAttribute('src');
|
for (const mutation of mutationsList) {
|
||||||
video.setAttribute('src', `${src},${nextSrc}`);
|
if (mutation.type === 'childList') {
|
||||||
}
|
for (const addedNode of mutation.addedNodes) {
|
||||||
});
|
if (addedNode.tagName === 'VIDEO') {
|
||||||
|
setVideoBase64(addedNode);
|
||||||
|
observer.disconnect(); // Stop observing once the video is handled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(slide, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Setup base64 videos
|
||||||
Reveal.on( 'ready', fixBase64VideoBackground );
|
Reveal.on( 'ready', fixBase64VideoBackground );
|
||||||
Reveal.on( 'slidechanged', fixBase64VideoBackground );
|
{% endif %}
|
||||||
{%- endif %}
|
|
||||||
</script>
|
</script>
|
||||||
|
{% if env['READTHEDOCS'] %}
|
||||||
{% if env['READTHEDOCS'] -%}
|
<style>
|
||||||
<style>
|
readthedocs-flyout, readthedocs-notification {
|
||||||
readthedocs-flyout, readthedocs-notification {
|
display: none;
|
||||||
display: none;
|
}
|
||||||
}
|
</style>
|
||||||
</style>
|
{% endif %}
|
||||||
{%- endif %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
from multiprocessing import Pool
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
import av
|
import av
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
|
|
||||||
def concatenate_video_files(files: list[Path], dest: Path) -> None:
|
def concatenate_video_files(files: list[Path], dest: Path) -> None:
|
||||||
"""Concatenate multiple video files into one."""
|
"""Concatenate multiple video files into one."""
|
||||||
|
if len(files) == 1:
|
||||||
|
shutil.copy(files[0], dest)
|
||||||
|
return
|
||||||
|
|
||||||
def _filter(files: list[Path]) -> Iterator[Path]:
|
def _filter(files: list[Path]) -> Iterator[Path]:
|
||||||
"""Patch possibly empty video files."""
|
"""Patch possibly empty video files."""
|
||||||
@ -19,7 +26,7 @@ def concatenate_video_files(files: list[Path], dest: Path) -> None:
|
|||||||
if len(container.streams.video) > 0:
|
if len(container.streams.video) > 0:
|
||||||
yield file
|
yield file
|
||||||
else:
|
else:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f"Skipping video file {file} because it does "
|
f"Skipping video file {file} because it does "
|
||||||
"not contain any video stream. "
|
"not contain any video stream. "
|
||||||
"This is probably caused by Manim, see: "
|
"This is probably caused by Manim, see: "
|
||||||
@ -89,8 +96,9 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
|
|||||||
c.link_to(n)
|
c.link_to(n)
|
||||||
|
|
||||||
|
|
||||||
def reverse_video_file(src: Path, dest: Path) -> None:
|
def reverse_video_file_in_one_chunk(src_and_dest: tuple[Path, Path]) -> None:
|
||||||
"""Reverses a video file, writing the result to `dest`."""
|
"""Reverses a video file, writing the result to `dest`."""
|
||||||
|
src, dest = src_and_dest
|
||||||
with (
|
with (
|
||||||
av.open(str(src)) as input_container,
|
av.open(str(src)) as input_container,
|
||||||
av.open(str(dest), mode="w") as output_container,
|
av.open(str(dest), mode="w") as output_container,
|
||||||
@ -120,8 +128,70 @@ def reverse_video_file(src: Path, dest: Path) -> None:
|
|||||||
|
|
||||||
for _ in range(frames_count):
|
for _ in range(frames_count):
|
||||||
frame = graph.pull()
|
frame = graph.pull()
|
||||||
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
|
frame.pict_type = "NONE" # Otherwise we get a warning saying it is changed
|
||||||
output_container.mux(output_stream.encode(frame))
|
output_container.mux(output_stream.encode(frame))
|
||||||
|
|
||||||
for packet in output_stream.encode():
|
for packet in output_stream.encode():
|
||||||
output_container.mux(packet)
|
output_container.mux(packet)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_video_file(
|
||||||
|
src: Path,
|
||||||
|
dest: Path,
|
||||||
|
max_segment_duration: Optional[float] = 4.0,
|
||||||
|
num_processes: Optional[int] = None,
|
||||||
|
**tqdm_kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Reverses a video file, writing the result to `dest`."""
|
||||||
|
with av.open(str(src)) as input_container: # Fast path if file is short enough
|
||||||
|
input_stream = input_container.streams.video[0]
|
||||||
|
if max_segment_duration is None:
|
||||||
|
return reverse_video_file_in_one_chunk((src, dest))
|
||||||
|
elif input_stream.duration:
|
||||||
|
if (
|
||||||
|
float(input_stream.duration * input_stream.time_base)
|
||||||
|
<= max_segment_duration
|
||||||
|
):
|
||||||
|
return reverse_video_file_in_one_chunk((src, dest))
|
||||||
|
else: # pragma: no cover
|
||||||
|
logger.debug(
|
||||||
|
f"Could not determine duration of {src}, falling back to segmentation."
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
tmpdir = Path(tmpdirname)
|
||||||
|
with av.open(
|
||||||
|
str(tmpdir / f"%04d.{src.suffix}"),
|
||||||
|
"w",
|
||||||
|
format="segment",
|
||||||
|
options={"segment_time": str(max_segment_duration)},
|
||||||
|
) as output_container:
|
||||||
|
output_stream = output_container.add_stream(
|
||||||
|
template=input_stream,
|
||||||
|
)
|
||||||
|
|
||||||
|
for packet in input_container.demux(input_stream):
|
||||||
|
if packet.dts is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
packet.stream = output_stream
|
||||||
|
output_container.mux(packet)
|
||||||
|
|
||||||
|
src_files = list(tmpdir.iterdir())
|
||||||
|
rev_files = [
|
||||||
|
src_file.with_stem("rev_" + src_file.stem) for src_file in src_files
|
||||||
|
]
|
||||||
|
|
||||||
|
with Pool(num_processes, maxtasksperchild=1) as pool:
|
||||||
|
for _ in tqdm(
|
||||||
|
pool.imap_unordered(
|
||||||
|
reverse_video_file_in_one_chunk, zip(src_files, rev_files)
|
||||||
|
),
|
||||||
|
desc="Reversing large file by cutting it in segments",
|
||||||
|
total=len(src_files),
|
||||||
|
unit=" files",
|
||||||
|
**tqdm_kwargs,
|
||||||
|
):
|
||||||
|
pass # We just consume the iterator
|
||||||
|
|
||||||
|
concatenate_video_files(rev_files[::-1], dest)
|
||||||
|
@ -63,7 +63,7 @@ class Wizard(QWidget): # type: ignore
|
|||||||
|
|
||||||
self.layout = QGridLayout()
|
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
|
# Create label for key name information
|
||||||
label = QLabel()
|
label = QLabel()
|
||||||
key_info = value["name"] or key
|
key_info = value["name"] or key
|
||||||
@ -97,7 +97,7 @@ class Wizard(QWidget): # type: ignore
|
|||||||
|
|
||||||
def save_config(self) -> None:
|
def save_config(self) -> None:
|
||||||
try:
|
try:
|
||||||
Config.model_validate(self.config.dict())
|
Config.model_validate(self.config.model_dump())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
msg.setIcon(QMessageBox.Critical)
|
msg.setIcon(QMessageBox.Critical)
|
||||||
|
166
pyproject.toml
166
pyproject.toml
@ -1,6 +1,39 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling", "hatch-fancy-pypi-readme"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
{include-group = "docs"},
|
||||||
|
{include-group = "tests"},
|
||||||
|
"bump-my-version>=0.20.3",
|
||||||
|
"pre-commit>=3.5.0",
|
||||||
|
]
|
||||||
|
docs = [
|
||||||
|
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
|
||||||
|
"furo>=2023.5.20",
|
||||||
|
"ipykernel>=6.25.1",
|
||||||
|
"myst-parser>=2.0.0",
|
||||||
|
"nbsphinx>=0.9.2",
|
||||||
|
"pandoc>=2.3",
|
||||||
|
"pygments<2.19", # See: https://github.com/ManimCommunity/manim/issues/4104
|
||||||
|
"sphinx>=7.0.1",
|
||||||
|
"sphinxcontrib-programoutput>=0.18",
|
||||||
|
"sphinx-design>=0.6.1",
|
||||||
|
"sphinx-click>=4.4.0",
|
||||||
|
"sphinx-copybutton>=0.5.1",
|
||||||
|
"sphinxext-opengraph>=0.7.5",
|
||||||
|
]
|
||||||
|
tests = [
|
||||||
|
"importlib-metadata>=8.6.1;python_version<'3.10'",
|
||||||
|
"manim-slides[full,manim,manimgl,pyqt6,pyside6,sphinx-directive]",
|
||||||
|
"pytest>=7.4.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
"pytest-env>=0.8.2",
|
||||||
|
"pytest-missing-modules>=0.1.0",
|
||||||
|
"pytest-qt>=4.2.0",
|
||||||
|
"setuptools>=73.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
|
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
|
||||||
@ -17,7 +50,8 @@ classifiers = [
|
|||||||
"Topic :: Scientific/Engineering",
|
"Topic :: Scientific/Engineering",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"av>=9.0.0",
|
"av>=9.0.0,<14",
|
||||||
|
"beautifulsoup4>=4.12.3",
|
||||||
"click>=8.1.3",
|
"click>=8.1.3",
|
||||||
"click-default-group>=1.2.2",
|
"click-default-group>=1.2.2",
|
||||||
"jinja2>=3.1.2",
|
"jinja2>=3.1.2",
|
||||||
@ -30,42 +64,26 @@ dependencies = [
|
|||||||
"qtpy>=2.4.1",
|
"qtpy>=2.4.1",
|
||||||
"requests>=2.28.1",
|
"requests>=2.28.1",
|
||||||
"rich>=13.3.2",
|
"rich>=13.3.2",
|
||||||
"rtoml>=0.9.0",
|
"rtoml>=0.11.0",
|
||||||
"tqdm>=4.64.1",
|
"tqdm>=4.64.1",
|
||||||
]
|
]
|
||||||
description = "Tool for live presentations using manim"
|
description = "Tool for live presentations using manim"
|
||||||
dynamic = ["version"]
|
dynamic = ["readme", "version"]
|
||||||
keywords = ["manim", "slides", "plugin", "manimgl"]
|
keywords = ["manim", "slides", "plugin", "manimgl"]
|
||||||
license = {text = "MIT"}
|
license = "MIT"
|
||||||
name = "manim-slides"
|
name = "manim-slides"
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
docs = [
|
|
||||||
"manim-slides[magic,sphinx-directive]",
|
|
||||||
"furo>=2023.5.20",
|
|
||||||
"ipykernel>=6.25.1",
|
|
||||||
"myst-parser>=2.0.0",
|
|
||||||
"nbsphinx>=0.9.2",
|
|
||||||
"pandoc>=2.3",
|
|
||||||
"sphinx>=7.0.1",
|
|
||||||
"sphinx-click>=4.4.0",
|
|
||||||
"sphinx-copybutton>=0.5.1",
|
|
||||||
"sphinxext-opengraph>=0.7.5",
|
|
||||||
]
|
|
||||||
full = [
|
full = [
|
||||||
"manim-slides[magic,manim,sphinx-directive]",
|
"manim-slides[magic,manim,sphinx-directive]",
|
||||||
]
|
]
|
||||||
full-gl = [
|
|
||||||
"manim-slides[magic,manimgl,sphinx-directive]",
|
|
||||||
]
|
|
||||||
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
|
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
|
||||||
manim = ["manim>=0.17.3"]
|
manim = ["manim>=0.19"]
|
||||||
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
|
manimgl = ["manimgl>=1.7.2"]
|
||||||
pyqt6 = ["pyqt6>=6.6.1"]
|
pyqt6 = ["pyqt6>=6.7.0"]
|
||||||
pyqt6-full = ["manim-slides[full,pyqt6]"]
|
pyqt6-full = ["manim-slides[full,pyqt6]"]
|
||||||
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
|
pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
|
||||||
pyside6-full = ["manim-slides[full,pyside6]"]
|
pyside6-full = ["manim-slides[full,pyside6]"]
|
||||||
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
||||||
|
|
||||||
@ -79,11 +97,61 @@ Founding = "https://github.com/sponsors/jeertmans"
|
|||||||
Homepage = "https://github.com/jeertmans/manim-slides"
|
Homepage = "https://github.com/jeertmans/manim-slides"
|
||||||
Repository = "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.5.1"
|
||||||
|
ignore_missing_version = false
|
||||||
|
message = "chore(deps): bump version from {current_version} to {new_version}"
|
||||||
|
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
|
||||||
|
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]
|
[tool.codespell]
|
||||||
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
|
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
|
||||||
check-hidden = true
|
check-hidden = true
|
||||||
ignore-words-list = "master"
|
ignore-words-list = "master"
|
||||||
skip = "requirements.lock,requirements-dev.lock"
|
skip = "uv.lock"
|
||||||
|
|
||||||
[tool.coverage.report]
|
[tool.coverage.report]
|
||||||
exclude_lines = [
|
exclude_lines = [
|
||||||
@ -94,6 +162,25 @@ exclude_lines = [
|
|||||||
]
|
]
|
||||||
precision = 2
|
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]
|
[tool.hatch.version]
|
||||||
path = "manim_slides/__version__.py"
|
path = "manim_slides/__version__.py"
|
||||||
|
|
||||||
@ -104,12 +191,20 @@ python_version = "3.9"
|
|||||||
strict = true
|
strict = true
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
addopts = [
|
||||||
|
"--cov-report=xml",
|
||||||
|
"--cov=manim_slides",
|
||||||
|
]
|
||||||
env = [
|
env = [
|
||||||
|
"QT_API=pyside6",
|
||||||
"QT_QPA_PLATFORM=offscreen",
|
"QT_QPA_PLATFORM=offscreen",
|
||||||
]
|
]
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
"error",
|
'''ignore:'audioop' is deprecated:DeprecationWarning''',
|
||||||
"ignore::DeprecationWarning",
|
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
|
||||||
|
'ignore::DeprecationWarning:pkg_resources.*:',
|
||||||
|
'ignore:invalid escape sequence.*:DeprecationWarning',
|
||||||
|
'ignore:invalid escape sequence.*:SyntaxWarning',
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
@ -136,15 +231,6 @@ extend-ignore = [
|
|||||||
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
|
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
|
||||||
isort = {known-first-party = ["manim_slides", "tests"]}
|
isort = {known-first-party = ["manim_slides", "tests"]}
|
||||||
|
|
||||||
[tool.rye]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
dev-dependencies = [
|
"docs/source/reference/magic_example.ipynb" = ["F403", "F405"]
|
||||||
"bump2version>=1.0.1",
|
"tests/test_slide.py" = ["N801"]
|
||||||
"manim-slides[manim,manimgl,pyqt6]",
|
|
||||||
"pre-commit>=3.5.0",
|
|
||||||
"pytest>=7.4.0",
|
|
||||||
"pytest-cov>=4.1.0",
|
|
||||||
"pytest-env>=0.8.2",
|
|
||||||
"pytest-qt>=4.2.0",
|
|
||||||
"pytest-xdist>=3.3.1",
|
|
||||||
]
|
|
||||||
managed = true
|
|
||||||
|
@ -1,419 +0,0 @@
|
|||||||
# generated by rye
|
|
||||||
# use `rye lock` or `rye sync` to update this lockfile
|
|
||||||
#
|
|
||||||
# last locked with the following flags:
|
|
||||||
# pre: false
|
|
||||||
# features: []
|
|
||||||
# all-features: true
|
|
||||||
# with-sources: false
|
|
||||||
|
|
||||||
-e file:.
|
|
||||||
alabaster==0.7.16
|
|
||||||
# via sphinx
|
|
||||||
annotated-types==0.6.0
|
|
||||||
# via pydantic
|
|
||||||
asttokens==2.4.1
|
|
||||||
# via stack-data
|
|
||||||
attrs==23.2.0
|
|
||||||
# via jsonschema
|
|
||||||
# via referencing
|
|
||||||
av==12.0.0
|
|
||||||
# via manim-slides
|
|
||||||
babel==2.14.0
|
|
||||||
# via sphinx
|
|
||||||
beautifulsoup4==4.12.3
|
|
||||||
# via furo
|
|
||||||
# via nbconvert
|
|
||||||
bleach==6.1.0
|
|
||||||
# via nbconvert
|
|
||||||
bump2version==1.0.1
|
|
||||||
certifi==2024.2.2
|
|
||||||
# via requests
|
|
||||||
cfgv==3.4.0
|
|
||||||
# via pre-commit
|
|
||||||
charset-normalizer==3.3.2
|
|
||||||
# via requests
|
|
||||||
click==8.1.7
|
|
||||||
# via click-default-group
|
|
||||||
# via cloup
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via sphinx-click
|
|
||||||
click-default-group==1.2.4
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
cloup==0.13.1
|
|
||||||
# via manim
|
|
||||||
colour==0.1.5
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
comm==0.2.2
|
|
||||||
# via ipykernel
|
|
||||||
contourpy==1.2.1
|
|
||||||
# via matplotlib
|
|
||||||
coverage==7.4.4
|
|
||||||
# via pytest-cov
|
|
||||||
cycler==0.12.1
|
|
||||||
# via matplotlib
|
|
||||||
debugpy==1.8.1
|
|
||||||
# via ipykernel
|
|
||||||
decorator==5.1.1
|
|
||||||
# via ipython
|
|
||||||
# via manim
|
|
||||||
defusedxml==0.7.1
|
|
||||||
# via nbconvert
|
|
||||||
distlib==0.3.8
|
|
||||||
# via virtualenv
|
|
||||||
docutils==0.20.1
|
|
||||||
# via manim-slides
|
|
||||||
# via myst-parser
|
|
||||||
# via nbsphinx
|
|
||||||
# via sphinx
|
|
||||||
# via sphinx-click
|
|
||||||
execnet==2.1.1
|
|
||||||
# via pytest-xdist
|
|
||||||
executing==2.0.1
|
|
||||||
# via stack-data
|
|
||||||
fastjsonschema==2.19.1
|
|
||||||
# via nbformat
|
|
||||||
filelock==3.13.4
|
|
||||||
# via virtualenv
|
|
||||||
fonttools==4.51.0
|
|
||||||
# via matplotlib
|
|
||||||
furo==2024.1.29
|
|
||||||
# via manim-slides
|
|
||||||
glcontext==2.5.0
|
|
||||||
# via moderngl
|
|
||||||
identify==2.5.35
|
|
||||||
# via pre-commit
|
|
||||||
idna==3.7
|
|
||||||
# via requests
|
|
||||||
imagesize==1.4.1
|
|
||||||
# via sphinx
|
|
||||||
iniconfig==2.0.0
|
|
||||||
# via pytest
|
|
||||||
ipykernel==6.29.4
|
|
||||||
# via manim-slides
|
|
||||||
ipython==8.18.1
|
|
||||||
# via ipykernel
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
isosurfaces==0.1.0
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
jedi==0.19.1
|
|
||||||
# via ipython
|
|
||||||
jinja2==3.1.3
|
|
||||||
# via manim-slides
|
|
||||||
# via myst-parser
|
|
||||||
# via nbconvert
|
|
||||||
# via nbsphinx
|
|
||||||
# via sphinx
|
|
||||||
jsonschema==4.21.1
|
|
||||||
# via nbformat
|
|
||||||
jsonschema-specifications==2023.12.1
|
|
||||||
# via jsonschema
|
|
||||||
jupyter-client==8.6.1
|
|
||||||
# via ipykernel
|
|
||||||
# via nbclient
|
|
||||||
jupyter-core==5.7.2
|
|
||||||
# via ipykernel
|
|
||||||
# via jupyter-client
|
|
||||||
# via nbclient
|
|
||||||
# via nbconvert
|
|
||||||
# via nbformat
|
|
||||||
jupyterlab-pygments==0.3.0
|
|
||||||
# via nbconvert
|
|
||||||
kiwisolver==1.4.5
|
|
||||||
# via matplotlib
|
|
||||||
lxml==5.2.1
|
|
||||||
# via manim-slides
|
|
||||||
# via python-pptx
|
|
||||||
manim==0.17.3
|
|
||||||
# via manim-slides
|
|
||||||
manimgl==1.6.1
|
|
||||||
# via manim-slides
|
|
||||||
manimpango==0.4.4
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
mapbox-earcut==1.0.1
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
markdown-it-py==3.0.0
|
|
||||||
# via mdit-py-plugins
|
|
||||||
# via myst-parser
|
|
||||||
# via rich
|
|
||||||
markupsafe==2.1.5
|
|
||||||
# via jinja2
|
|
||||||
# via nbconvert
|
|
||||||
matplotlib==3.8.4
|
|
||||||
# via manimgl
|
|
||||||
matplotlib-inline==0.1.7
|
|
||||||
# via ipykernel
|
|
||||||
# via ipython
|
|
||||||
mdit-py-plugins==0.4.0
|
|
||||||
# via myst-parser
|
|
||||||
mdurl==0.1.2
|
|
||||||
# via markdown-it-py
|
|
||||||
mistune==3.0.2
|
|
||||||
# via nbconvert
|
|
||||||
moderngl==5.10.0
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
# via moderngl-window
|
|
||||||
moderngl-window==2.4.4
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
mpmath==1.3.0
|
|
||||||
# via sympy
|
|
||||||
multipledispatch==1.0.0
|
|
||||||
# via pyrr
|
|
||||||
myst-parser==2.0.0
|
|
||||||
# via manim-slides
|
|
||||||
nbclient==0.10.0
|
|
||||||
# via nbconvert
|
|
||||||
nbconvert==7.16.3
|
|
||||||
# via nbsphinx
|
|
||||||
nbformat==5.10.4
|
|
||||||
# via nbclient
|
|
||||||
# via nbconvert
|
|
||||||
# via nbsphinx
|
|
||||||
nbsphinx==0.9.3
|
|
||||||
# via manim-slides
|
|
||||||
nest-asyncio==1.6.0
|
|
||||||
# via ipykernel
|
|
||||||
networkx==2.8.8
|
|
||||||
# via manim
|
|
||||||
nodeenv==1.8.0
|
|
||||||
# via pre-commit
|
|
||||||
numpy==1.26.4
|
|
||||||
# via contourpy
|
|
||||||
# via isosurfaces
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
# via mapbox-earcut
|
|
||||||
# via matplotlib
|
|
||||||
# via moderngl-window
|
|
||||||
# via pyrr
|
|
||||||
# via scipy
|
|
||||||
packaging==24.0
|
|
||||||
# via ipykernel
|
|
||||||
# via matplotlib
|
|
||||||
# via nbconvert
|
|
||||||
# via pytest
|
|
||||||
# via qtpy
|
|
||||||
# via sphinx
|
|
||||||
pandoc==2.3
|
|
||||||
# via manim-slides
|
|
||||||
pandocfilters==1.5.1
|
|
||||||
# via nbconvert
|
|
||||||
parso==0.8.4
|
|
||||||
# via jedi
|
|
||||||
pexpect==4.9.0
|
|
||||||
# via ipython
|
|
||||||
pillow==9.5.0
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
# via matplotlib
|
|
||||||
# via moderngl-window
|
|
||||||
# via python-pptx
|
|
||||||
platformdirs==4.2.0
|
|
||||||
# via jupyter-core
|
|
||||||
# via virtualenv
|
|
||||||
pluggy==1.4.0
|
|
||||||
# via pytest
|
|
||||||
# via pytest-qt
|
|
||||||
plumbum==1.8.2
|
|
||||||
# via pandoc
|
|
||||||
ply==3.11
|
|
||||||
# via pandoc
|
|
||||||
pre-commit==3.7.0
|
|
||||||
prompt-toolkit==3.0.43
|
|
||||||
# via ipython
|
|
||||||
psutil==5.9.8
|
|
||||||
# via ipykernel
|
|
||||||
ptyprocess==0.7.0
|
|
||||||
# via pexpect
|
|
||||||
pure-eval==0.2.2
|
|
||||||
# via stack-data
|
|
||||||
pycairo==1.26.0
|
|
||||||
# via manim
|
|
||||||
pydantic==2.7.0
|
|
||||||
# via manim-slides
|
|
||||||
# via pydantic-extra-types
|
|
||||||
pydantic-core==2.18.1
|
|
||||||
# via pydantic
|
|
||||||
pydantic-extra-types==2.6.0
|
|
||||||
# via manim-slides
|
|
||||||
pydub==0.25.1
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
pyglet==2.0.15
|
|
||||||
# via moderngl-window
|
|
||||||
pygments==2.17.2
|
|
||||||
# via furo
|
|
||||||
# via ipython
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
# via nbconvert
|
|
||||||
# via rich
|
|
||||||
# via sphinx
|
|
||||||
pyopengl==3.1.7
|
|
||||||
# via manimgl
|
|
||||||
pyparsing==3.1.2
|
|
||||||
# via matplotlib
|
|
||||||
pyqt6==6.6.1
|
|
||||||
# via manim-slides
|
|
||||||
pyqt6-qt6==6.6.3
|
|
||||||
# via pyqt6
|
|
||||||
pyqt6-sip==13.6.0
|
|
||||||
# via pyqt6
|
|
||||||
pyrr==0.10.3
|
|
||||||
# via moderngl-window
|
|
||||||
pyside6==6.5.2
|
|
||||||
# via manim-slides
|
|
||||||
pyside6-addons==6.5.2
|
|
||||||
# via pyside6
|
|
||||||
pyside6-essentials==6.5.2
|
|
||||||
# via pyside6
|
|
||||||
# via pyside6-addons
|
|
||||||
pytest==8.1.1
|
|
||||||
# via pytest-cov
|
|
||||||
# via pytest-env
|
|
||||||
# via pytest-qt
|
|
||||||
# via pytest-xdist
|
|
||||||
pytest-cov==5.0.0
|
|
||||||
pytest-env==1.1.3
|
|
||||||
pytest-qt==4.4.0
|
|
||||||
pytest-xdist==3.5.0
|
|
||||||
python-dateutil==2.9.0.post0
|
|
||||||
# via jupyter-client
|
|
||||||
# via matplotlib
|
|
||||||
python-pptx==0.6.23
|
|
||||||
# via manim-slides
|
|
||||||
pyyaml==6.0.1
|
|
||||||
# via manimgl
|
|
||||||
# via myst-parser
|
|
||||||
# via pre-commit
|
|
||||||
pyzmq==26.0.0
|
|
||||||
# via ipykernel
|
|
||||||
# via jupyter-client
|
|
||||||
qtpy==2.4.1
|
|
||||||
# via manim-slides
|
|
||||||
referencing==0.34.0
|
|
||||||
# via jsonschema
|
|
||||||
# via jsonschema-specifications
|
|
||||||
requests==2.31.0
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via sphinx
|
|
||||||
rich==13.7.1
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
rpds-py==0.18.0
|
|
||||||
# via jsonschema
|
|
||||||
# via referencing
|
|
||||||
rtoml==0.10.0
|
|
||||||
# via manim-slides
|
|
||||||
scipy==1.13.0
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
screeninfo==0.8.1
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
setuptools==69.5.1
|
|
||||||
# via nodeenv
|
|
||||||
shiboken6==6.5.2
|
|
||||||
# via pyside6
|
|
||||||
# via pyside6-addons
|
|
||||||
# via pyside6-essentials
|
|
||||||
six==1.16.0
|
|
||||||
# via asttokens
|
|
||||||
# via bleach
|
|
||||||
# via python-dateutil
|
|
||||||
skia-pathops==0.7.4
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
snowballstemmer==2.2.0
|
|
||||||
# via sphinx
|
|
||||||
soupsieve==2.5
|
|
||||||
# via beautifulsoup4
|
|
||||||
sphinx==7.3.6
|
|
||||||
# via furo
|
|
||||||
# via manim-slides
|
|
||||||
# via myst-parser
|
|
||||||
# via nbsphinx
|
|
||||||
# via sphinx-basic-ng
|
|
||||||
# via sphinx-click
|
|
||||||
# via sphinx-copybutton
|
|
||||||
# via sphinxext-opengraph
|
|
||||||
sphinx-basic-ng==1.0.0b2
|
|
||||||
# via furo
|
|
||||||
sphinx-click==5.1.0
|
|
||||||
# via manim-slides
|
|
||||||
sphinx-copybutton==0.5.2
|
|
||||||
# via manim-slides
|
|
||||||
sphinxcontrib-applehelp==1.0.8
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-devhelp==1.0.6
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-htmlhelp==2.0.5
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-jsmath==1.0.1
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-qthelp==1.0.7
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-serializinghtml==1.1.10
|
|
||||||
# via sphinx
|
|
||||||
sphinxext-opengraph==0.9.1
|
|
||||||
# via manim-slides
|
|
||||||
srt==3.5.3
|
|
||||||
# via manim
|
|
||||||
stack-data==0.6.3
|
|
||||||
# via ipython
|
|
||||||
svgelements==1.9.6
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
sympy==1.12
|
|
||||||
# via manimgl
|
|
||||||
tinycss2==1.2.1
|
|
||||||
# via nbconvert
|
|
||||||
tornado==6.4
|
|
||||||
# via ipykernel
|
|
||||||
# via jupyter-client
|
|
||||||
tqdm==4.66.2
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
traitlets==5.14.2
|
|
||||||
# via comm
|
|
||||||
# via ipykernel
|
|
||||||
# via ipython
|
|
||||||
# via jupyter-client
|
|
||||||
# via jupyter-core
|
|
||||||
# via matplotlib-inline
|
|
||||||
# via nbclient
|
|
||||||
# via nbconvert
|
|
||||||
# via nbformat
|
|
||||||
# via nbsphinx
|
|
||||||
typing-extensions==4.11.0
|
|
||||||
# via pydantic
|
|
||||||
# via pydantic-core
|
|
||||||
urllib3==2.2.1
|
|
||||||
# via requests
|
|
||||||
validators==0.28.0
|
|
||||||
# via manimgl
|
|
||||||
virtualenv==20.25.3
|
|
||||||
# via pre-commit
|
|
||||||
watchdog==2.3.1
|
|
||||||
# via manim
|
|
||||||
wcwidth==0.2.13
|
|
||||||
# via prompt-toolkit
|
|
||||||
webencodings==0.5.1
|
|
||||||
# via bleach
|
|
||||||
# via tinycss2
|
|
||||||
xlsxwriter==3.2.0
|
|
||||||
# via python-pptx
|
|
@ -1,382 +0,0 @@
|
|||||||
# generated by rye
|
|
||||||
# use `rye lock` or `rye sync` to update this lockfile
|
|
||||||
#
|
|
||||||
# last locked with the following flags:
|
|
||||||
# pre: false
|
|
||||||
# features: []
|
|
||||||
# all-features: true
|
|
||||||
# with-sources: false
|
|
||||||
|
|
||||||
-e file:.
|
|
||||||
alabaster==0.7.16
|
|
||||||
# via sphinx
|
|
||||||
annotated-types==0.6.0
|
|
||||||
# via pydantic
|
|
||||||
asttokens==2.4.1
|
|
||||||
# via stack-data
|
|
||||||
attrs==23.2.0
|
|
||||||
# via jsonschema
|
|
||||||
# via referencing
|
|
||||||
av==12.0.0
|
|
||||||
# via manim-slides
|
|
||||||
babel==2.14.0
|
|
||||||
# via sphinx
|
|
||||||
beautifulsoup4==4.12.3
|
|
||||||
# via furo
|
|
||||||
# via nbconvert
|
|
||||||
bleach==6.1.0
|
|
||||||
# via nbconvert
|
|
||||||
certifi==2024.2.2
|
|
||||||
# via requests
|
|
||||||
charset-normalizer==3.3.2
|
|
||||||
# via requests
|
|
||||||
click==8.1.7
|
|
||||||
# via click-default-group
|
|
||||||
# via cloup
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via sphinx-click
|
|
||||||
click-default-group==1.2.4
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
cloup==0.13.1
|
|
||||||
# via manim
|
|
||||||
colour==0.1.5
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
comm==0.2.2
|
|
||||||
# via ipykernel
|
|
||||||
contourpy==1.2.1
|
|
||||||
# via matplotlib
|
|
||||||
cycler==0.12.1
|
|
||||||
# via matplotlib
|
|
||||||
debugpy==1.8.1
|
|
||||||
# via ipykernel
|
|
||||||
decorator==5.1.1
|
|
||||||
# via ipython
|
|
||||||
# via manim
|
|
||||||
defusedxml==0.7.1
|
|
||||||
# via nbconvert
|
|
||||||
docutils==0.20.1
|
|
||||||
# via manim-slides
|
|
||||||
# via myst-parser
|
|
||||||
# via nbsphinx
|
|
||||||
# via sphinx
|
|
||||||
# via sphinx-click
|
|
||||||
executing==2.0.1
|
|
||||||
# via stack-data
|
|
||||||
fastjsonschema==2.19.1
|
|
||||||
# via nbformat
|
|
||||||
fonttools==4.51.0
|
|
||||||
# via matplotlib
|
|
||||||
furo==2024.1.29
|
|
||||||
# via manim-slides
|
|
||||||
glcontext==2.5.0
|
|
||||||
# via moderngl
|
|
||||||
idna==3.7
|
|
||||||
# via requests
|
|
||||||
imagesize==1.4.1
|
|
||||||
# via sphinx
|
|
||||||
ipykernel==6.29.4
|
|
||||||
# via manim-slides
|
|
||||||
ipython==8.18.1
|
|
||||||
# via ipykernel
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
isosurfaces==0.1.0
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
jedi==0.19.1
|
|
||||||
# via ipython
|
|
||||||
jinja2==3.1.3
|
|
||||||
# via manim-slides
|
|
||||||
# via myst-parser
|
|
||||||
# via nbconvert
|
|
||||||
# via nbsphinx
|
|
||||||
# via sphinx
|
|
||||||
jsonschema==4.21.1
|
|
||||||
# via nbformat
|
|
||||||
jsonschema-specifications==2023.12.1
|
|
||||||
# via jsonschema
|
|
||||||
jupyter-client==8.6.1
|
|
||||||
# via ipykernel
|
|
||||||
# via nbclient
|
|
||||||
jupyter-core==5.7.2
|
|
||||||
# via ipykernel
|
|
||||||
# via jupyter-client
|
|
||||||
# via nbclient
|
|
||||||
# via nbconvert
|
|
||||||
# via nbformat
|
|
||||||
jupyterlab-pygments==0.3.0
|
|
||||||
# via nbconvert
|
|
||||||
kiwisolver==1.4.5
|
|
||||||
# via matplotlib
|
|
||||||
lxml==5.2.1
|
|
||||||
# via manim-slides
|
|
||||||
# via python-pptx
|
|
||||||
manim==0.17.3
|
|
||||||
# via manim-slides
|
|
||||||
manimgl==1.6.1
|
|
||||||
# via manim-slides
|
|
||||||
manimpango==0.4.4
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
mapbox-earcut==1.0.1
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
markdown-it-py==3.0.0
|
|
||||||
# via mdit-py-plugins
|
|
||||||
# via myst-parser
|
|
||||||
# via rich
|
|
||||||
markupsafe==2.1.5
|
|
||||||
# via jinja2
|
|
||||||
# via nbconvert
|
|
||||||
matplotlib==3.8.4
|
|
||||||
# via manimgl
|
|
||||||
matplotlib-inline==0.1.7
|
|
||||||
# via ipykernel
|
|
||||||
# via ipython
|
|
||||||
mdit-py-plugins==0.4.0
|
|
||||||
# via myst-parser
|
|
||||||
mdurl==0.1.2
|
|
||||||
# via markdown-it-py
|
|
||||||
mistune==3.0.2
|
|
||||||
# via nbconvert
|
|
||||||
moderngl==5.10.0
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
# via moderngl-window
|
|
||||||
moderngl-window==2.4.4
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
mpmath==1.3.0
|
|
||||||
# via sympy
|
|
||||||
multipledispatch==1.0.0
|
|
||||||
# via pyrr
|
|
||||||
myst-parser==2.0.0
|
|
||||||
# via manim-slides
|
|
||||||
nbclient==0.10.0
|
|
||||||
# via nbconvert
|
|
||||||
nbconvert==7.16.3
|
|
||||||
# via nbsphinx
|
|
||||||
nbformat==5.10.4
|
|
||||||
# via nbclient
|
|
||||||
# via nbconvert
|
|
||||||
# via nbsphinx
|
|
||||||
nbsphinx==0.9.3
|
|
||||||
# via manim-slides
|
|
||||||
nest-asyncio==1.6.0
|
|
||||||
# via ipykernel
|
|
||||||
networkx==2.8.8
|
|
||||||
# via manim
|
|
||||||
numpy==1.26.4
|
|
||||||
# via contourpy
|
|
||||||
# via isosurfaces
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
# via mapbox-earcut
|
|
||||||
# via matplotlib
|
|
||||||
# via moderngl-window
|
|
||||||
# via pyrr
|
|
||||||
# via scipy
|
|
||||||
packaging==24.0
|
|
||||||
# via ipykernel
|
|
||||||
# via matplotlib
|
|
||||||
# via nbconvert
|
|
||||||
# via qtpy
|
|
||||||
# via sphinx
|
|
||||||
pandoc==2.3
|
|
||||||
# via manim-slides
|
|
||||||
pandocfilters==1.5.1
|
|
||||||
# via nbconvert
|
|
||||||
parso==0.8.4
|
|
||||||
# via jedi
|
|
||||||
pexpect==4.9.0
|
|
||||||
# via ipython
|
|
||||||
pillow==9.5.0
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
# via matplotlib
|
|
||||||
# via moderngl-window
|
|
||||||
# via python-pptx
|
|
||||||
platformdirs==4.2.0
|
|
||||||
# via jupyter-core
|
|
||||||
plumbum==1.8.2
|
|
||||||
# via pandoc
|
|
||||||
ply==3.11
|
|
||||||
# via pandoc
|
|
||||||
prompt-toolkit==3.0.43
|
|
||||||
# via ipython
|
|
||||||
psutil==5.9.8
|
|
||||||
# via ipykernel
|
|
||||||
ptyprocess==0.7.0
|
|
||||||
# via pexpect
|
|
||||||
pure-eval==0.2.2
|
|
||||||
# via stack-data
|
|
||||||
pycairo==1.26.0
|
|
||||||
# via manim
|
|
||||||
pydantic==2.7.0
|
|
||||||
# via manim-slides
|
|
||||||
# via pydantic-extra-types
|
|
||||||
pydantic-core==2.18.1
|
|
||||||
# via pydantic
|
|
||||||
pydantic-extra-types==2.6.0
|
|
||||||
# via manim-slides
|
|
||||||
pydub==0.25.1
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
pyglet==2.0.15
|
|
||||||
# via moderngl-window
|
|
||||||
pygments==2.17.2
|
|
||||||
# via furo
|
|
||||||
# via ipython
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
# via nbconvert
|
|
||||||
# via rich
|
|
||||||
# via sphinx
|
|
||||||
pyopengl==3.1.7
|
|
||||||
# via manimgl
|
|
||||||
pyparsing==3.1.2
|
|
||||||
# via matplotlib
|
|
||||||
pyqt6==6.6.1
|
|
||||||
# via manim-slides
|
|
||||||
pyqt6-qt6==6.6.3
|
|
||||||
# via pyqt6
|
|
||||||
pyqt6-sip==13.6.0
|
|
||||||
# via pyqt6
|
|
||||||
pyrr==0.10.3
|
|
||||||
# via moderngl-window
|
|
||||||
pyside6==6.5.2
|
|
||||||
# via manim-slides
|
|
||||||
pyside6-addons==6.5.2
|
|
||||||
# via pyside6
|
|
||||||
pyside6-essentials==6.5.2
|
|
||||||
# via pyside6
|
|
||||||
# via pyside6-addons
|
|
||||||
python-dateutil==2.9.0.post0
|
|
||||||
# via jupyter-client
|
|
||||||
# via matplotlib
|
|
||||||
python-pptx==0.6.23
|
|
||||||
# via manim-slides
|
|
||||||
pyyaml==6.0.1
|
|
||||||
# via manimgl
|
|
||||||
# via myst-parser
|
|
||||||
pyzmq==26.0.0
|
|
||||||
# via ipykernel
|
|
||||||
# via jupyter-client
|
|
||||||
qtpy==2.4.1
|
|
||||||
# via manim-slides
|
|
||||||
referencing==0.34.0
|
|
||||||
# via jsonschema
|
|
||||||
# via jsonschema-specifications
|
|
||||||
requests==2.31.0
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via sphinx
|
|
||||||
rich==13.7.1
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
rpds-py==0.18.0
|
|
||||||
# via jsonschema
|
|
||||||
# via referencing
|
|
||||||
rtoml==0.10.0
|
|
||||||
# via manim-slides
|
|
||||||
scipy==1.13.0
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
screeninfo==0.8.1
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
shiboken6==6.5.2
|
|
||||||
# via pyside6
|
|
||||||
# via pyside6-addons
|
|
||||||
# via pyside6-essentials
|
|
||||||
six==1.16.0
|
|
||||||
# via asttokens
|
|
||||||
# via bleach
|
|
||||||
# via python-dateutil
|
|
||||||
skia-pathops==0.7.4
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
snowballstemmer==2.2.0
|
|
||||||
# via sphinx
|
|
||||||
soupsieve==2.5
|
|
||||||
# via beautifulsoup4
|
|
||||||
sphinx==7.3.6
|
|
||||||
# via furo
|
|
||||||
# via manim-slides
|
|
||||||
# via myst-parser
|
|
||||||
# via nbsphinx
|
|
||||||
# via sphinx-basic-ng
|
|
||||||
# via sphinx-click
|
|
||||||
# via sphinx-copybutton
|
|
||||||
# via sphinxext-opengraph
|
|
||||||
sphinx-basic-ng==1.0.0b2
|
|
||||||
# via furo
|
|
||||||
sphinx-click==5.1.0
|
|
||||||
# via manim-slides
|
|
||||||
sphinx-copybutton==0.5.2
|
|
||||||
# via manim-slides
|
|
||||||
sphinxcontrib-applehelp==1.0.8
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-devhelp==1.0.6
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-htmlhelp==2.0.5
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-jsmath==1.0.1
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-qthelp==1.0.7
|
|
||||||
# via sphinx
|
|
||||||
sphinxcontrib-serializinghtml==1.1.10
|
|
||||||
# via sphinx
|
|
||||||
sphinxext-opengraph==0.9.1
|
|
||||||
# via manim-slides
|
|
||||||
srt==3.5.3
|
|
||||||
# via manim
|
|
||||||
stack-data==0.6.3
|
|
||||||
# via ipython
|
|
||||||
svgelements==1.9.6
|
|
||||||
# via manim
|
|
||||||
# via manimgl
|
|
||||||
sympy==1.12
|
|
||||||
# via manimgl
|
|
||||||
tinycss2==1.2.1
|
|
||||||
# via nbconvert
|
|
||||||
tornado==6.4
|
|
||||||
# via ipykernel
|
|
||||||
# via jupyter-client
|
|
||||||
tqdm==4.66.2
|
|
||||||
# via manim
|
|
||||||
# via manim-slides
|
|
||||||
# via manimgl
|
|
||||||
traitlets==5.14.2
|
|
||||||
# via comm
|
|
||||||
# via ipykernel
|
|
||||||
# via ipython
|
|
||||||
# via jupyter-client
|
|
||||||
# via jupyter-core
|
|
||||||
# via matplotlib-inline
|
|
||||||
# via nbclient
|
|
||||||
# via nbconvert
|
|
||||||
# via nbformat
|
|
||||||
# via nbsphinx
|
|
||||||
typing-extensions==4.11.0
|
|
||||||
# via pydantic
|
|
||||||
# via pydantic-core
|
|
||||||
urllib3==2.2.1
|
|
||||||
# via requests
|
|
||||||
validators==0.28.0
|
|
||||||
# via manimgl
|
|
||||||
watchdog==2.3.1
|
|
||||||
# via manim
|
|
||||||
wcwidth==0.2.13
|
|
||||||
# via prompt-toolkit
|
|
||||||
webencodings==0.5.1
|
|
||||||
# via bleach
|
|
||||||
# via tinycss2
|
|
||||||
xlsxwriter==3.2.0
|
|
||||||
# via python-pptx
|
|
@ -1,5 +1,5 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
|
||||||
rye run manim-slides render -t -qk -s --format png --resolution 64,64 static/logo.py ManimSlidesFavicon && mv media/images/logo/*.png static/favicon.png
|
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
|
ln -f -r -s static/favicon.png docs/source/_static/favicon.png
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
|
||||||
MANIM_SLIDES_THEME=light rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && 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
|
ln -f -r -s static/logo.png docs/source/_static/logo.png
|
||||||
|
|
||||||
MANIM_SLIDES_THEME=dark_docs rye 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
|
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
|
ln -f -r -s static/logo_dark_docs.png docs/source/_static/logo_dark_docs.png
|
||||||
|
|
||||||
MANIM_SLIDES_THEME=dark_github rye 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
|
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
|
ln -f -r -s static/logo_dark_github.png docs/source/_static/logo_dark_github.png
|
||||||
|
|
||||||
MANIM_SLIDES_THEME=light rye 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
|
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
|
ln -f -r -s static/logo_light_transparent.png docs/source/_static/logo_light_transparent.png
|
||||||
|
|
||||||
MANIM_SLIDES_THEME=dark_docs rye 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
|
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
|
ln -f -r -s static/logo_dark_transparent.png docs/source/_static/logo_dark_transparent.png
|
||||||
|
@ -71,8 +71,20 @@ def paths() -> Generator[list[Path], None, None]:
|
|||||||
yield [random_path() for _ in range(20)]
|
yield [random_path() for _ in range(20)]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture
|
||||||
def presentation_config(
|
def presentation_config(
|
||||||
slides_folder: Path,
|
slides_folder: Path,
|
||||||
) -> Generator[PresentationConfig, None, None]:
|
) -> Generator[PresentationConfig, None, None]:
|
||||||
yield PresentationConfig.from_file(slides_folder / "BasicSlide.json")
|
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,12 @@ class BasicSlide(Slide):
|
|||||||
|
|
||||||
self.next_slide()
|
self.next_slide()
|
||||||
self.zoom(other_text, [])
|
self.zoom(other_text, [])
|
||||||
|
|
||||||
|
|
||||||
|
class BasicSlideSkipReversing(BasicSlide):
|
||||||
|
skip_reversing = True
|
||||||
|
|
||||||
|
|
||||||
|
class FailingSlide(Slide):
|
||||||
|
def construct(self):
|
||||||
|
self.play("this fails to render")
|
||||||
|
@ -86,6 +86,15 @@ class TestBaseSlide:
|
|||||||
|
|
||||||
assert base_slide.wait_time_between_slides == 0.0
|
assert base_slide.wait_time_between_slides == 0.0
|
||||||
|
|
||||||
|
def test_skip_animations(self, base_slide: BaseSlide) -> None:
|
||||||
|
assert not base_slide._skip_animations
|
||||||
|
|
||||||
|
def test_start_and_stop_skip_animations(self, base_slide: BaseSlide) -> None:
|
||||||
|
base_slide.start_skip_animations()
|
||||||
|
assert base_slide._skip_animations
|
||||||
|
base_slide.stop_skip_animations()
|
||||||
|
assert not base_slide._skip_animations
|
||||||
|
|
||||||
def test_play(self) -> None:
|
def test_play(self) -> None:
|
||||||
pass # This method should be tested in test_slide.py
|
pass # This method should be tested in test_slide.py
|
||||||
|
|
||||||
|
73
tests/test_checkhealth.py
Normal file
73
tests/test_checkhealth.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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):
|
||||||
|
if (
|
||||||
|
not manimlib_missing
|
||||||
|
and not MANIMGL_NOT_INSTALLED
|
||||||
|
and sys.version_info < (3, 10)
|
||||||
|
):
|
||||||
|
pytest.skip("See https://github.com/3b1b/manim/issues/2263")
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
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,8 +1,10 @@
|
|||||||
|
import shutil
|
||||||
from enum import EnumMeta
|
from enum import EnumMeta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from manim_slides.config import PresentationConfig
|
from manim_slides.config import PresentationConfig
|
||||||
from manim_slides.convert import (
|
from manim_slides.convert import (
|
||||||
@ -17,6 +19,7 @@ from manim_slides.convert import (
|
|||||||
ControlsLayout,
|
ControlsLayout,
|
||||||
Converter,
|
Converter,
|
||||||
Display,
|
Display,
|
||||||
|
HtmlZip,
|
||||||
JsBool,
|
JsBool,
|
||||||
JsFalse,
|
JsFalse,
|
||||||
JsNull,
|
JsNull,
|
||||||
@ -138,7 +141,8 @@ def test_unquoted_enum(enum_type: EnumMeta) -> None:
|
|||||||
|
|
||||||
class TestConverter:
|
class TestConverter:
|
||||||
@pytest.mark.parametrize(
|
@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:
|
def test_from_string(self, name: str, converter: type) -> None:
|
||||||
assert Converter.from_string(name) == converter
|
assert Converter.from_string(name) == converter
|
||||||
@ -150,9 +154,164 @@ class TestConverter:
|
|||||||
RevealJS(presentation_configs=[presentation_config]).convert_to(out_file)
|
RevealJS(presentation_configs=[presentation_config]).convert_to(out_file)
|
||||||
assert out_file.exists()
|
assert out_file.exists()
|
||||||
assert Path(tmp_path / "slides_assets").is_dir()
|
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()
|
assert "manim" in file_contents.casefold()
|
||||||
|
|
||||||
|
def test_revealjs_offline_converter(
|
||||||
|
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||||
|
) -> None:
|
||||||
|
out_file = tmp_path / "slides.html"
|
||||||
|
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
|
||||||
|
out_file
|
||||||
|
)
|
||||||
|
assert out_file.exists()
|
||||||
|
assets_dir = Path(tmp_path / "slides_assets")
|
||||||
|
assert assets_dir.is_dir()
|
||||||
|
for file in [
|
||||||
|
"black.min.css",
|
||||||
|
"reveal.min.css",
|
||||||
|
"reveal.min.js",
|
||||||
|
"zenburn.min.css",
|
||||||
|
]:
|
||||||
|
assert (assets_dir / file).exists()
|
||||||
|
|
||||||
|
def test_revealjs_data_encode(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
presentation_config: PresentationConfig,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# Mock requests.Session.get to return a fake response (should not be called)
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, content: bytes, text: str, status_code: int) -> None:
|
||||||
|
self.content = content
|
||||||
|
self.text = text
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
monkeypatch.setattr(
|
||||||
|
requests.Session,
|
||||||
|
"get",
|
||||||
|
lambda self, url: MockResponse(
|
||||||
|
b"body { background-color: #9a3241; }",
|
||||||
|
"body { background-color: #9a3241; }",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
out_file = tmp_path / "slides.html"
|
||||||
|
RevealJS(
|
||||||
|
presentation_configs=[presentation_config], offline="false", one_file="true"
|
||||||
|
).convert_to(out_file)
|
||||||
|
assert out_file.exists()
|
||||||
|
# Check that assets are not stored
|
||||||
|
assert not (tmp_path / "slides_assets").exists()
|
||||||
|
|
||||||
|
with open(out_file, encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
|
||||||
|
# Check if video is encoded in base64
|
||||||
|
videos = soup.find_all("section")
|
||||||
|
assert all(
|
||||||
|
"data:video/mp4;base64," in video["data-background-video"]
|
||||||
|
for video in videos
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if CSS is not inlined
|
||||||
|
styles = soup.find_all("style")
|
||||||
|
assert not any("background-color: #9a3241;" in style.string for style in styles)
|
||||||
|
# Check if JS is not inlined
|
||||||
|
scripts = soup.find_all("script")
|
||||||
|
assert not any(
|
||||||
|
"background-color: #9a3241;" in (script.string or "") for script in scripts
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_revealjs_offline_inlining(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
presentation_config: PresentationConfig,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# Mock requests.Session.get to return a fake response
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, content: bytes, text: str, status_code: int) -> None:
|
||||||
|
self.content = content
|
||||||
|
self.text = text
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
monkeypatch.setattr(
|
||||||
|
requests.Session,
|
||||||
|
"get",
|
||||||
|
lambda self, url: MockResponse(
|
||||||
|
b"body { background-color: #9a3241; }",
|
||||||
|
"body { background-color: #9a3241; }",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
out_file = tmp_path / "slides.html"
|
||||||
|
RevealJS(
|
||||||
|
presentation_configs=[presentation_config], offline="true", one_file="true"
|
||||||
|
).convert_to(out_file)
|
||||||
|
assert out_file.exists()
|
||||||
|
|
||||||
|
with open(out_file, encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
|
||||||
|
# Check if CSS is inlined
|
||||||
|
styles = soup.find_all("style")
|
||||||
|
assert any("background-color: #9a3241;" in style.string for style in styles)
|
||||||
|
|
||||||
|
# Check if JS is inlined
|
||||||
|
scripts = soup.find_all("script")
|
||||||
|
assert any("background-color: #9a3241;" in script.string for script in scripts)
|
||||||
|
|
||||||
|
def test_htmlzip_converter(
|
||||||
|
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"))
|
@pytest.mark.parametrize("frame_index", ("first", "last"))
|
||||||
def test_pdf_converter(
|
def test_pdf_converter(
|
||||||
self, frame_index: str, tmp_path: Path, presentation_config: PresentationConfig
|
self, frame_index: str, tmp_path: Path, presentation_config: PresentationConfig
|
||||||
@ -163,10 +322,6 @@ class TestConverter:
|
|||||||
).convert_to(out_file)
|
).convert_to(out_file)
|
||||||
assert out_file.exists()
|
assert out_file.exists()
|
||||||
|
|
||||||
def test_converter_no_presentation_config(self) -> None:
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
Converter(presentation_configs=[])
|
|
||||||
|
|
||||||
def test_pptx_converter(
|
def test_pptx_converter(
|
||||||
self, tmp_path: Path, presentation_config: PresentationConfig
|
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -64,6 +65,34 @@ def test_convert(slides_folder: Path, extension: str) -> None:
|
|||||||
assert results.exit_code == 0
|
assert results.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("extension",), [("html",)])
|
||||||
|
def test_convert_data_uri_deprecated(slides_folder: Path, extension: str) -> None:
|
||||||
|
runner = CliRunner(mix_stderr=False)
|
||||||
|
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
results = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"convert",
|
||||||
|
"BasicSlide",
|
||||||
|
f"basic_example.{extension}",
|
||||||
|
"--folder",
|
||||||
|
str(slides_folder),
|
||||||
|
"--to",
|
||||||
|
extension,
|
||||||
|
"-cdata_uri=true",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert any(
|
||||||
|
"'data_uri' configuration option is deprecated" in str(item.message)
|
||||||
|
and item.category is DeprecationWarning
|
||||||
|
for item in w
|
||||||
|
)
|
||||||
|
assert results.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("extension", "expected_log"),
|
("extension", "expected_log"),
|
||||||
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],
|
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],
|
||||||
|
@ -6,6 +6,11 @@ import pytest
|
|||||||
|
|
||||||
import manim_slides.slide as slide
|
import manim_slides.slide as slide
|
||||||
|
|
||||||
|
skip_if_py39 = pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 10),
|
||||||
|
reason="See https://github.com/3b1b/manim/issues/2263",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_import(
|
def assert_import(
|
||||||
*,
|
*,
|
||||||
@ -20,6 +25,7 @@ def assert_import(
|
|||||||
assert slide.MANIMGL == manimgl
|
assert slide.MANIMGL == manimgl
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_py39
|
||||||
def test_force_api() -> None:
|
def test_force_api() -> None:
|
||||||
pytest.importorskip("manimlib")
|
pytest.importorskip("manimlib")
|
||||||
import manim # noqa: F401
|
import manim # noqa: F401
|
||||||
@ -53,6 +59,7 @@ def test_invalid_api() -> None:
|
|||||||
del os.environ[slide.MANIM_API]
|
del os.environ[slide.MANIM_API]
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_py39
|
||||||
@pytest.mark.filterwarnings("ignore:assert_import")
|
@pytest.mark.filterwarnings("ignore:assert_import")
|
||||||
def test_manim_and_manimgl_imported() -> None:
|
def test_manim_and_manimgl_imported() -> None:
|
||||||
pytest.importorskip("manimlib")
|
pytest.importorskip("manimlib")
|
||||||
@ -79,6 +86,7 @@ def test_manim_imported() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_py39
|
||||||
def test_manimgl_imported() -> None:
|
def test_manimgl_imported() -> None:
|
||||||
pytest.importorskip("manimlib")
|
pytest.importorskip("manimlib")
|
||||||
import manimlib # noqa: F401
|
import manimlib # noqa: F401
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
|
import contextlib
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections.abc import Iterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Union
|
||||||
|
|
||||||
import manim
|
|
||||||
import numpy as np
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from manim import (
|
from manim import (
|
||||||
BLACK,
|
|
||||||
BLUE,
|
BLUE,
|
||||||
DOWN,
|
DOWN,
|
||||||
LEFT,
|
LEFT,
|
||||||
@ -19,15 +21,42 @@ from manim import (
|
|||||||
Dot,
|
Dot,
|
||||||
FadeIn,
|
FadeIn,
|
||||||
GrowFromCenter,
|
GrowFromCenter,
|
||||||
|
Square,
|
||||||
Text,
|
Text,
|
||||||
)
|
)
|
||||||
from packaging import version
|
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
from manim_slides.config import PresentationConfig
|
from manim_slides.config import PresentationConfig
|
||||||
from manim_slides.defaults import FOLDER_PATH
|
from manim_slides.defaults import FOLDER_PATH
|
||||||
from manim_slides.render import render
|
from manim_slides.render import render
|
||||||
from manim_slides.slide.manim import Slide
|
from manim_slides.slide.manim import Slide as CESlide
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
|
||||||
|
class _GLSlide:
|
||||||
|
def construct(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
GLSlide = pytest.param(
|
||||||
|
_GLSlide,
|
||||||
|
marks=pytest.mark.skip(reason="See https://github.com/3b1b/manim/issues/2263"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from manim_slides.slide.manimlib import Slide as GLSlide
|
||||||
|
|
||||||
|
_GLSlide = GLSlide
|
||||||
|
|
||||||
|
|
||||||
|
class CEGLSlide(CESlide):
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
SlideType = Union[type[CESlide], type[_GLSlide], type[CEGLSlide]]
|
||||||
|
Slide = Union[CESlide, _GLSlide, CEGLSlide]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -37,12 +66,13 @@ from manim_slides.slide.manim import Slide
|
|||||||
pytest.param(
|
pytest.param(
|
||||||
"--GL",
|
"--GL",
|
||||||
marks=pytest.mark.skipif(
|
marks=pytest.mark.skipif(
|
||||||
version.parse(np.__version__) >= version.parse("1.25")
|
sys.version_info < (3, 10),
|
||||||
or sys.version_info >= (3, 12),
|
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
"--CE --renderer=opengl",
|
||||||
],
|
],
|
||||||
|
ids=("CE", "GL", "CE(GL)"),
|
||||||
)
|
)
|
||||||
def test_render_basic_slide(
|
def test_render_basic_slide(
|
||||||
renderer: str,
|
renderer: str,
|
||||||
@ -55,7 +85,7 @@ def test_render_basic_slide(
|
|||||||
with runner.isolated_filesystem() as tmp_dir:
|
with runner.isolated_filesystem() as tmp_dir:
|
||||||
shutil.copy(manimgl_config, tmp_dir)
|
shutil.copy(manimgl_config, tmp_dir)
|
||||||
results = runner.invoke(
|
results = runner.invoke(
|
||||||
render, [renderer, str(slides_file), "BasicSlide", "-ql"]
|
render, [*renderer.split(" "), str(slides_file), "BasicSlide", "-ql"]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert results.exit_code == 0, results
|
assert results.exit_code == 0, results
|
||||||
@ -83,272 +113,593 @@ def test_render_basic_slide(
|
|||||||
assert local_presentation_config.resolution == presentation_config.resolution
|
assert local_presentation_config.resolution == presentation_config.resolution
|
||||||
|
|
||||||
|
|
||||||
def assert_constructs(cls: type) -> type:
|
@pytest.mark.parametrize(
|
||||||
class Wrapper:
|
"renderer",
|
||||||
@classmethod
|
[
|
||||||
def test_construct(_) -> None: # noqa: N804
|
"--CE",
|
||||||
cls().construct()
|
pytest.param(
|
||||||
|
"--GL",
|
||||||
|
marks=pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 10),
|
||||||
|
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"--CE --renderer=opengl",
|
||||||
|
],
|
||||||
|
ids=("CE", "GL", "CE(GL)"),
|
||||||
|
)
|
||||||
|
def test_render_failing_slide(
|
||||||
|
renderer: str,
|
||||||
|
slides_file: Path,
|
||||||
|
manimgl_config: Path,
|
||||||
|
) -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
return Wrapper
|
with runner.isolated_filesystem() as tmp_dir:
|
||||||
|
shutil.copy(manimgl_config, tmp_dir)
|
||||||
|
results = runner.invoke(
|
||||||
|
render, [*renderer.split(" "), str(slides_file), "FailingSlide", "-ql"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert results.exit_code != 0, results
|
||||||
|
|
||||||
|
|
||||||
def assert_renders(cls: type) -> type:
|
def test_clear_cache(
|
||||||
class Wrapper:
|
slides_file: Path,
|
||||||
@classmethod
|
) -> None:
|
||||||
def test_render(_) -> None: # noqa: N804
|
runner = CliRunner()
|
||||||
cls().render()
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"renderer",
|
||||||
|
[
|
||||||
|
"--CE",
|
||||||
|
pytest.param(
|
||||||
|
"--GL",
|
||||||
|
marks=pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 10),
|
||||||
|
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@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()
|
||||||
|
|
||||||
|
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 parse_cli
|
||||||
|
|
||||||
|
_args = parse_cli()
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def tmp_cwd() -> Iterator[str]:
|
||||||
|
cwd = os.getcwd()
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
os.chdir(tmp_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield tmp_dir
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_renders(cls: SlideType) -> None:
|
||||||
|
with tmp_cwd():
|
||||||
|
init_slide(cls).render()
|
||||||
|
|
||||||
|
|
||||||
class TestSlide:
|
class TestSlide:
|
||||||
@assert_constructs
|
def test_default_properties(self) -> None:
|
||||||
class TestDefaultProperties(Slide):
|
@assert_constructs
|
||||||
def construct(self) -> None:
|
class _(CESlide):
|
||||||
assert self._output_folder == FOLDER_PATH
|
def construct(self) -> None:
|
||||||
assert len(self._slides) == 0
|
assert self._output_folder == FOLDER_PATH
|
||||||
assert self._current_slide == 1
|
assert len(self._slides) == 0
|
||||||
assert self._start_animation == 0
|
assert self._current_slide == 1
|
||||||
assert len(self._canvas) == 0
|
assert self._start_animation == 0
|
||||||
assert self._wait_time_between_slides == 0.0
|
assert len(self._canvas) == 0
|
||||||
|
assert self._wait_time_between_slides == 0.0
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@parametrize_base_cls
|
||||||
version.parse(manim.__version__) < version.parse("0.18"),
|
def test_frame_height(self, base_cls: SlideType) -> None:
|
||||||
reason="Manim change how color are represented in 0.18",
|
@assert_constructs
|
||||||
)
|
class _(base_cls): # type: ignore
|
||||||
@assert_constructs
|
def construct(self) -> None:
|
||||||
class TestBackgroundColor(Slide):
|
assert self._frame_height > 0 and isinstance(self._frame_height, float)
|
||||||
def construct(self) -> None:
|
|
||||||
assert self._background_color == BLACK.to_hex() # DEFAULT
|
|
||||||
self.camera.background_color = BLUE
|
|
||||||
assert self._background_color == BLUE.to_hex()
|
|
||||||
|
|
||||||
@assert_renders
|
@parametrize_base_cls
|
||||||
class TestMultipleAnimationsInLastSlide(Slide):
|
def test_frame_width(self, base_cls: SlideType) -> None:
|
||||||
"""Check against solution for issue #161."""
|
@assert_constructs
|
||||||
|
class _(base_cls): # type: ignore
|
||||||
|
def construct(self) -> None:
|
||||||
|
assert self._frame_width > 0 and isinstance(self._frame_width, float)
|
||||||
|
|
||||||
def construct(self) -> None:
|
@parametrize_base_cls
|
||||||
circle = Circle(color=BLUE)
|
def test_resolution(self, base_cls: SlideType) -> None:
|
||||||
dot = Dot()
|
@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(GrowFromCenter(circle))
|
@parametrize_base_cls
|
||||||
self.play(FadeIn(dot))
|
def test_backround_color(self, base_cls: SlideType) -> None:
|
||||||
self.next_slide()
|
@assert_constructs
|
||||||
|
class _(base_cls): # type: ignore
|
||||||
|
def construct(self) -> None:
|
||||||
|
assert self._background_color in ["#000000", "#000"] # DEFAULT
|
||||||
|
|
||||||
self.play(dot.animate.move_to(RIGHT))
|
def test_multiple_animations_in_last_slide(self) -> None:
|
||||||
self.play(dot.animate.move_to(UP))
|
@assert_renders
|
||||||
self.play(dot.animate.move_to(LEFT))
|
class _(CESlide):
|
||||||
self.play(dot.animate.move_to(DOWN))
|
"""Check against solution for issue #161."""
|
||||||
|
|
||||||
@assert_renders
|
def construct(self) -> None:
|
||||||
class TestFileTooLong(Slide):
|
circle = Circle(color=BLUE)
|
||||||
"""Check against solution for issue #123."""
|
dot = Dot()
|
||||||
|
|
||||||
def construct(self) -> None:
|
self.play(GrowFromCenter(circle))
|
||||||
circle = Circle(radius=3, color=BLUE)
|
self.play(FadeIn(dot))
|
||||||
dot = Dot()
|
|
||||||
self.play(GrowFromCenter(circle), run_time=0.1)
|
|
||||||
|
|
||||||
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.next_slide()
|
self.next_slide()
|
||||||
|
|
||||||
@assert_constructs
|
self.play(dot.animate.move_to(RIGHT))
|
||||||
class TestPlaybackRate(Slide):
|
self.play(dot.animate.move_to(UP))
|
||||||
def construct(self) -> None:
|
self.play(dot.animate.move_to(LEFT))
|
||||||
text = Text("Some text")
|
self.play(dot.animate.move_to(DOWN))
|
||||||
|
|
||||||
self.add(text)
|
def test_split_reverse(self) -> None:
|
||||||
|
@assert_renders
|
||||||
|
class _(CESlide):
|
||||||
|
max_duration_before_split_reverse = 3.0
|
||||||
|
|
||||||
assert self._base_slide_config.playback_rate == 1.0
|
def construct(self) -> None:
|
||||||
|
self.wait(2.0)
|
||||||
|
for _ in range(3):
|
||||||
|
self.next_slide()
|
||||||
|
self.wait(10.0)
|
||||||
|
|
||||||
self.next_slide(playback_rate=2.0)
|
@assert_renders
|
||||||
self.play(text.animate.scale(2))
|
class __(CESlide):
|
||||||
|
max_duration_before_split_reverse = None
|
||||||
|
|
||||||
assert self._base_slide_config.playback_rate == 2.0
|
def construct(self) -> None:
|
||||||
|
self.wait(5.0)
|
||||||
|
self.next_slide()
|
||||||
|
self.wait(5.0)
|
||||||
|
|
||||||
@assert_constructs
|
def test_file_too_long(self) -> None:
|
||||||
class TestReversedPlaybackRate(Slide):
|
@assert_renders
|
||||||
def construct(self) -> None:
|
class _(CESlide):
|
||||||
text = Text("Some text")
|
"""Check against solution for issue #123."""
|
||||||
|
|
||||||
self.add(text)
|
def construct(self) -> None:
|
||||||
|
circle = Circle(radius=3, color=BLUE)
|
||||||
|
dot = Dot()
|
||||||
|
self.play(GrowFromCenter(circle), run_time=0.1)
|
||||||
|
|
||||||
assert self._base_slide_config.reversed_playback_rate == 1.0
|
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)
|
||||||
|
|
||||||
self.next_slide(reversed_playback_rate=2.0)
|
def test_loop(self) -> None:
|
||||||
self.play(text.animate.scale(2))
|
@assert_constructs
|
||||||
|
class _(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
text = Text("Some text")
|
||||||
|
|
||||||
assert self._base_slide_config.reversed_playback_rate == 2.0
|
self.add(text)
|
||||||
|
|
||||||
@assert_constructs
|
assert not self._base_slide_config.loop
|
||||||
class TestNotes(Slide):
|
|
||||||
def construct(self) -> None:
|
|
||||||
text = Text("Some text")
|
|
||||||
|
|
||||||
self.add(text)
|
self.next_slide(loop=True)
|
||||||
|
self.play(text.animate.scale(2))
|
||||||
|
|
||||||
assert self._base_slide_config.notes == ""
|
assert self._base_slide_config.loop
|
||||||
|
|
||||||
self.next_slide(notes="test")
|
self.next_slide(loop=False)
|
||||||
self.play(text.animate.scale(2))
|
|
||||||
|
|
||||||
assert self._base_slide_config.notes == "test"
|
assert not self._base_slide_config.loop
|
||||||
|
|
||||||
@assert_constructs
|
def test_auto_next(self) -> None:
|
||||||
class TestWipe(Slide):
|
@assert_constructs
|
||||||
def construct(self) -> None:
|
class _(CESlide):
|
||||||
text = Text("Some text")
|
def construct(self) -> None:
|
||||||
bye = Text("Bye")
|
text = Text("Some text")
|
||||||
|
|
||||||
self.add(text)
|
self.add(text)
|
||||||
|
|
||||||
assert text in self.mobjects
|
assert not self._base_slide_config.auto_next
|
||||||
assert bye not in self.mobjects
|
|
||||||
|
|
||||||
self.wipe([text], [bye])
|
self.next_slide(auto_next=True)
|
||||||
|
self.play(text.animate.scale(2))
|
||||||
|
|
||||||
assert text not in self.mobjects
|
assert self._base_slide_config.auto_next
|
||||||
assert bye in self.mobjects
|
|
||||||
|
|
||||||
@assert_constructs
|
self.next_slide(auto_next=False)
|
||||||
class TestZoom(Slide):
|
|
||||||
def construct(self) -> None:
|
|
||||||
text = Text("Some text")
|
|
||||||
bye = Text("Bye")
|
|
||||||
|
|
||||||
self.add(text)
|
assert not self._base_slide_config.auto_next
|
||||||
|
|
||||||
assert text in self.mobjects
|
def test_loop_and_auto_next_succeeds(self) -> None:
|
||||||
assert bye not in self.mobjects
|
@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
|
self.next_slide(loop=True, auto_next=True)
|
||||||
assert bye in self.mobjects
|
self.play(text.animate.scale(2))
|
||||||
|
|
||||||
@assert_constructs
|
self.next_slide()
|
||||||
class TestPlay(Slide):
|
|
||||||
def construct(self) -> None:
|
|
||||||
assert self._current_animation == 0
|
|
||||||
circle = Circle(color=BLUE)
|
|
||||||
dot = Dot()
|
|
||||||
|
|
||||||
self.play(GrowFromCenter(circle))
|
def test_playback_rate(self) -> None:
|
||||||
assert self._current_animation == 1
|
@assert_constructs
|
||||||
self.play(FadeIn(dot))
|
class _(CESlide):
|
||||||
assert self._current_animation == 2
|
def construct(self) -> None:
|
||||||
|
text = Text("Some text")
|
||||||
|
|
||||||
@assert_constructs
|
self.add(text)
|
||||||
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
|
|
||||||
|
|
||||||
@assert_constructs
|
assert self._base_slide_config.playback_rate == 1.0
|
||||||
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
|
|
||||||
|
|
||||||
@assert_constructs
|
self.next_slide(playback_rate=2.0)
|
||||||
class TestCanvas(Slide):
|
self.play(text.animate.scale(2))
|
||||||
def construct(self) -> None:
|
|
||||||
text = Text("Some text")
|
|
||||||
bye = Text("Bye")
|
|
||||||
|
|
||||||
assert len(self.canvas) == 0
|
assert self._base_slide_config.playback_rate == 2.0
|
||||||
|
|
||||||
self.add(text)
|
def test_reversed_playback_rate(self) -> None:
|
||||||
|
@assert_constructs
|
||||||
|
class _(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
text = Text("Some text")
|
||||||
|
|
||||||
assert len(self.canvas) == 0
|
self.add(text)
|
||||||
|
|
||||||
self.add_to_canvas(text=text)
|
assert self._base_slide_config.reversed_playback_rate == 1.0
|
||||||
|
|
||||||
assert len(self.canvas) == 1
|
self.next_slide(reversed_playback_rate=2.0)
|
||||||
|
self.play(text.animate.scale(2))
|
||||||
|
|
||||||
self.add(bye)
|
assert self._base_slide_config.reversed_playback_rate == 2.0
|
||||||
|
|
||||||
assert len(self.canvas) == 1
|
def test_notes(self) -> None:
|
||||||
|
@assert_constructs
|
||||||
|
class _(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
text = Text("Some text")
|
||||||
|
|
||||||
assert text not in self.mobjects_without_canvas
|
self.add(text)
|
||||||
assert bye in self.mobjects_without_canvas
|
|
||||||
|
|
||||||
self.remove(text)
|
assert self._base_slide_config.notes == ""
|
||||||
|
|
||||||
assert len(self.canvas) == 1
|
self.next_slide(notes="test")
|
||||||
|
self.play(text.animate.scale(2))
|
||||||
|
|
||||||
self.add_to_canvas(bye=bye)
|
assert self._base_slide_config.notes == "test"
|
||||||
|
|
||||||
assert len(self.canvas) == 2
|
def test_wipe(self) -> None:
|
||||||
|
@assert_constructs
|
||||||
|
class _(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
text = Text("Some text")
|
||||||
|
bye = Text("Bye")
|
||||||
|
|
||||||
self.remove_from_canvas("text", "bye")
|
self.add(text)
|
||||||
|
|
||||||
assert len(self.canvas) == 0
|
assert text in self.mobjects
|
||||||
|
assert bye not in self.mobjects
|
||||||
|
|
||||||
with pytest.raises(KeyError):
|
self.wipe([text], [bye])
|
||||||
self.remove_from_canvas("text")
|
|
||||||
|
assert text not in self.mobjects
|
||||||
|
assert bye in self.mobjects
|
||||||
|
|
||||||
|
def test_zoom(self) -> None:
|
||||||
|
@assert_constructs
|
||||||
|
class _(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
text = Text("Some text")
|
||||||
|
bye = Text("Bye")
|
||||||
|
|
||||||
|
self.add(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_next_slide_skip_animations(self) -> None:
|
||||||
|
class Foo(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
circle = Circle(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(circle))
|
||||||
|
assert not self._base_slide_config.skip_animations
|
||||||
|
self.next_slide(skip_animations=True)
|
||||||
|
square = Square(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
assert self._base_slide_config.skip_animations
|
||||||
|
self.next_slide()
|
||||||
|
assert not self._base_slide_config.skip_animations
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
|
||||||
|
class Bar(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
circle = Circle(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(circle))
|
||||||
|
assert not self._base_slide_config.skip_animations
|
||||||
|
self.next_slide(skip_animations=False)
|
||||||
|
square = Square(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
assert not self._base_slide_config.skip_animations
|
||||||
|
self.next_slide()
|
||||||
|
assert not self._base_slide_config.skip_animations
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
|
||||||
|
class Baz(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
circle = Circle(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(circle))
|
||||||
|
assert not self._base_slide_config.skip_animations
|
||||||
|
self.start_skip_animations()
|
||||||
|
self.next_slide()
|
||||||
|
square = Square(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
assert self._base_slide_config.skip_animations
|
||||||
|
self.next_slide()
|
||||||
|
assert self._base_slide_config.skip_animations
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
self.stop_skip_animations()
|
||||||
|
|
||||||
|
with tmp_cwd() as tmp_dir:
|
||||||
|
init_slide(Foo).render()
|
||||||
|
init_slide(Bar).render()
|
||||||
|
init_slide(Baz).render()
|
||||||
|
|
||||||
|
slides_folder = Path(tmp_dir) / "slides"
|
||||||
|
|
||||||
|
assert slides_folder.exists()
|
||||||
|
|
||||||
|
slide_file = slides_folder / "Foo.json"
|
||||||
|
|
||||||
|
config = PresentationConfig.from_file(slide_file)
|
||||||
|
|
||||||
|
assert len(config.slides) == 2
|
||||||
|
|
||||||
|
slide_file = slides_folder / "Bar.json"
|
||||||
|
|
||||||
|
config = PresentationConfig.from_file(slide_file)
|
||||||
|
|
||||||
|
assert len(config.slides) == 3
|
||||||
|
|
||||||
|
slide_file = slides_folder / "Baz.json"
|
||||||
|
|
||||||
|
config = PresentationConfig.from_file(slide_file)
|
||||||
|
|
||||||
|
assert len(config.slides) == 1
|
||||||
|
|
||||||
|
def test_next_slide_include_video(self) -> None:
|
||||||
|
class Foo(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
circle = Circle(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(circle))
|
||||||
|
self.next_slide()
|
||||||
|
square = Square(color=BLUE)
|
||||||
|
self.play(GrowFromCenter(square))
|
||||||
|
self.next_slide()
|
||||||
|
self.wait(2)
|
||||||
|
|
||||||
|
with tmp_cwd() as tmp_dir:
|
||||||
|
init_slide(Foo).render()
|
||||||
|
|
||||||
|
slides_folder = Path(tmp_dir) / "slides"
|
||||||
|
|
||||||
|
assert slides_folder.exists()
|
||||||
|
|
||||||
|
slide_file = slides_folder / "Foo.json"
|
||||||
|
|
||||||
|
config = PresentationConfig.from_file(slide_file)
|
||||||
|
|
||||||
|
assert len(config.slides) == 3
|
||||||
|
|
||||||
|
class Bar(CESlide):
|
||||||
|
def construct(self) -> None:
|
||||||
|
self.next_slide(src=config.slides[0].file)
|
||||||
|
self.wait(2)
|
||||||
|
self.next_slide()
|
||||||
|
self.wait(2)
|
||||||
|
self.next_slide() # Dummy
|
||||||
|
self.next_slide(src=config.slides[1].file, loop=True)
|
||||||
|
self.next_slide() # Dummy
|
||||||
|
self.wait(2)
|
||||||
|
self.next_slide(src=config.slides[2].file)
|
||||||
|
|
||||||
|
init_slide(Bar).render()
|
||||||
|
|
||||||
|
slide_file = slides_folder / "Bar.json"
|
||||||
|
|
||||||
|
config = PresentationConfig.from_file(slide_file)
|
||||||
|
|
||||||
|
assert len(config.slides) == 6
|
||||||
|
assert config.slides[-3].loop
|
||||||
|
|
||||||
|
def test_canvas(self) -> None:
|
||||||
|
@assert_constructs
|
||||||
|
class _(CESlide):
|
||||||
|
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")
|
||||||
|
@ -69,7 +69,7 @@ class TestWizard:
|
|||||||
|
|
||||||
monkeypatch.setattr(QMessageBox, "exec", exec_patched)
|
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))
|
open_dialog(i, getattr(wizard.config.keys, key))
|
||||||
|
|
||||||
wizard.button_box.accepted.emit()
|
wizard.button_box.accepted.emit()
|
||||||
@ -89,7 +89,7 @@ def test_init() -> None:
|
|||||||
|
|
||||||
assert results.exit_code == 0
|
assert results.exit_code == 0
|
||||||
assert CONFIG_PATH.exists()
|
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:
|
def test_init_custom_path() -> None:
|
||||||
@ -106,7 +106,7 @@ def test_init_custom_path() -> None:
|
|||||||
assert results.exit_code == 0
|
assert results.exit_code == 0
|
||||||
assert not CONFIG_PATH.exists()
|
assert not CONFIG_PATH.exists()
|
||||||
assert custom_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:
|
def test_init_path_exists() -> None:
|
||||||
@ -120,7 +120,7 @@ def test_init_path_exists() -> None:
|
|||||||
|
|
||||||
assert results.exit_code == 0
|
assert results.exit_code == 0
|
||||||
assert CONFIG_PATH.exists()
|
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")
|
results = runner.invoke(init, input="o")
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ def test_wizard(monkeypatch: MonkeyPatch) -> None:
|
|||||||
|
|
||||||
assert results.exit_code == 0
|
assert results.exit_code == 0
|
||||||
assert CONFIG_PATH.exists()
|
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:
|
def test_wizard_closed_without_saving(monkeypatch: MonkeyPatch) -> None:
|
||||||
|
Reference in New Issue
Block a user